mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
307 lines
8 KiB
TypeScript
307 lines
8 KiB
TypeScript
// src/lib/integrations/meta/MetaBaseClient.ts
|
|
// Base Client für Meta Graph API (Facebook + Instagram)
|
|
|
|
import type {
|
|
MetaUser,
|
|
FacebookPage,
|
|
InstagramBusinessAccount,
|
|
MetaPaginatedResponse,
|
|
MetaErrorResponse,
|
|
} from '@/types/meta'
|
|
import { META_CONFIG } from './oauth'
|
|
|
|
// =============================================================================
|
|
// Error Types
|
|
// =============================================================================
|
|
|
|
export class MetaApiError extends Error {
|
|
code: number
|
|
subcode?: number
|
|
type: string
|
|
fbtraceId: string
|
|
|
|
constructor(error: MetaErrorResponse['error']) {
|
|
super(error.message)
|
|
this.name = 'MetaApiError'
|
|
this.code = error.code
|
|
this.subcode = error.error_subcode
|
|
this.type = error.type
|
|
this.fbtraceId = error.fbtrace_id
|
|
}
|
|
|
|
isRateLimitError(): boolean {
|
|
return this.code === 4 || this.code === 17 || this.code === 32
|
|
}
|
|
|
|
isAuthError(): boolean {
|
|
return this.code === 190 || this.code === 102
|
|
}
|
|
|
|
isPermissionError(): boolean {
|
|
return this.code === 10 || this.code === 200 || this.code === 230
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Base Client
|
|
// =============================================================================
|
|
|
|
export class MetaBaseClient {
|
|
protected accessToken: string
|
|
protected apiVersion: string
|
|
protected baseUrl: string
|
|
|
|
constructor(accessToken: string) {
|
|
this.accessToken = accessToken
|
|
this.apiVersion = META_CONFIG.apiVersion
|
|
this.baseUrl = META_CONFIG.baseUrl
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Core Request Method
|
|
// ===========================================================================
|
|
|
|
protected async request<T>(
|
|
endpoint: string,
|
|
options: {
|
|
method?: 'GET' | 'POST' | 'DELETE'
|
|
params?: Record<string, string | number | boolean | undefined>
|
|
body?: Record<string, unknown>
|
|
} = {}
|
|
): Promise<T> {
|
|
const { method = 'GET', params = {}, body } = options
|
|
|
|
// Build URL with params
|
|
const url = new URL(`${this.baseUrl}/${this.apiVersion}/${endpoint}`)
|
|
|
|
// Add access token
|
|
url.searchParams.set('access_token', this.accessToken)
|
|
|
|
// Add other params
|
|
for (const [key, value] of Object.entries(params)) {
|
|
if (value !== undefined) {
|
|
url.searchParams.set(key, String(value))
|
|
}
|
|
}
|
|
|
|
// Build request options
|
|
const fetchOptions: RequestInit = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}
|
|
|
|
if (body && method !== 'GET') {
|
|
fetchOptions.body = JSON.stringify(body)
|
|
}
|
|
|
|
// Execute request
|
|
const response = await fetch(url.toString(), fetchOptions)
|
|
const data = await response.json()
|
|
|
|
// Handle errors
|
|
if (!response.ok || data.error) {
|
|
const error = data.error || {
|
|
message: 'Unknown error',
|
|
type: 'UnknownError',
|
|
code: response.status,
|
|
fbtrace_id: 'unknown',
|
|
}
|
|
throw new MetaApiError(error)
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Paginated Request Helper
|
|
// ===========================================================================
|
|
|
|
protected async requestPaginated<T>(
|
|
endpoint: string,
|
|
options: {
|
|
params?: Record<string, string | number | boolean | undefined>
|
|
limit?: number
|
|
maxPages?: number
|
|
} = {}
|
|
): Promise<T[]> {
|
|
const { params = {}, limit = 100, maxPages = 10 } = options
|
|
const allItems: T[] = []
|
|
let pageCount = 0
|
|
let nextUrl: string | undefined
|
|
|
|
// Initial request
|
|
const initialParams = { ...params, limit }
|
|
const response = await this.request<MetaPaginatedResponse<T>>(endpoint, {
|
|
params: initialParams,
|
|
})
|
|
|
|
allItems.push(...response.data)
|
|
nextUrl = response.paging?.next
|
|
pageCount++
|
|
|
|
// Fetch additional pages
|
|
while (nextUrl && pageCount < maxPages) {
|
|
const nextResponse = await fetch(nextUrl)
|
|
const nextData: MetaPaginatedResponse<T> = await nextResponse.json()
|
|
|
|
if ('error' in (nextData as unknown as MetaErrorResponse)) {
|
|
throw new MetaApiError((nextData as unknown as MetaErrorResponse).error)
|
|
}
|
|
|
|
allItems.push(...nextData.data)
|
|
nextUrl = nextData.paging?.next
|
|
pageCount++
|
|
|
|
// Rate limiting
|
|
await this.sleep(100)
|
|
}
|
|
|
|
return allItems
|
|
}
|
|
|
|
// ===========================================================================
|
|
// User & Account Methods
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Hole Informationen über den authentifizierten User
|
|
*/
|
|
async getMe(): Promise<MetaUser> {
|
|
return this.request<MetaUser>('me', {
|
|
params: {
|
|
fields: 'id,name,email',
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Hole alle Facebook Pages des Users
|
|
*/
|
|
async getPages(): Promise<FacebookPage[]> {
|
|
const response = await this.request<MetaPaginatedResponse<FacebookPage>>(
|
|
'me/accounts',
|
|
{
|
|
params: {
|
|
fields:
|
|
'id,name,access_token,category,category_list,tasks,instagram_business_account',
|
|
},
|
|
}
|
|
)
|
|
return response.data
|
|
}
|
|
|
|
/**
|
|
* Hole Instagram Business Account für eine Page
|
|
*/
|
|
async getInstagramAccount(pageId: string): Promise<InstagramBusinessAccount | null> {
|
|
try {
|
|
const response = await this.request<{
|
|
instagram_business_account?: InstagramBusinessAccount
|
|
}>(`${pageId}`, {
|
|
params: {
|
|
fields:
|
|
'instagram_business_account{id,username,name,profile_picture_url,followers_count,follows_count,media_count,biography,website}',
|
|
},
|
|
})
|
|
|
|
return response.instagram_business_account || null
|
|
} catch (error) {
|
|
if (error instanceof MetaApiError && error.isPermissionError()) {
|
|
return null
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hole alle Instagram Business Accounts des Users (über alle Pages)
|
|
*/
|
|
async getAllInstagramAccounts(): Promise<
|
|
Array<{
|
|
page: FacebookPage
|
|
instagram: InstagramBusinessAccount
|
|
}>
|
|
> {
|
|
const pages = await this.getPages()
|
|
const results: Array<{
|
|
page: FacebookPage
|
|
instagram: InstagramBusinessAccount
|
|
}> = []
|
|
|
|
for (const page of pages) {
|
|
if (page.instagram_business_account?.id) {
|
|
const instagram = await this.getInstagramAccount(page.id)
|
|
if (instagram) {
|
|
results.push({ page, instagram })
|
|
}
|
|
}
|
|
|
|
// Rate limiting
|
|
await this.sleep(100)
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Token Management
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Aktualisiert den Access Token
|
|
*/
|
|
setAccessToken(token: string): void {
|
|
this.accessToken = token
|
|
}
|
|
|
|
/**
|
|
* Gibt den aktuellen Access Token zurück
|
|
*/
|
|
getAccessToken(): string {
|
|
return this.accessToken
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Utility Methods
|
|
// ===========================================================================
|
|
|
|
protected sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
/**
|
|
* Rate Limiting Helper - wartet bei Rate Limit Errors
|
|
*/
|
|
protected async withRetry<T>(
|
|
fn: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
initialDelay: number = 1000
|
|
): Promise<T> {
|
|
let lastError: Error | undefined
|
|
let delay = initialDelay
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
return await fn()
|
|
} catch (error) {
|
|
lastError = error as Error
|
|
|
|
if (error instanceof MetaApiError && error.isRateLimitError()) {
|
|
console.warn(
|
|
`[MetaClient] Rate limit hit, waiting ${delay}ms before retry ${attempt + 1}/${maxRetries}`
|
|
)
|
|
await this.sleep(delay)
|
|
delay *= 2 // Exponential backoff
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError
|
|
}
|
|
}
|
|
|
|
export default MetaBaseClient
|