import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApnsClient, SilentNotification, Notification, Errors } from 'apns2'; import * as fs from 'fs'; import { ApnsConfig, ApnsNotificationOptions } from './interfaces/apns-config.interface'; interface SendResult { sent: string[]; failed: Array<{ device: string; error?: Error; status?: string; response?: any; }>; } @Injectable() export class ApnsProvider implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(ApnsProvider.name); private client: ApnsClient; private config: ApnsConfig; constructor(private readonly configService: ConfigService) { this.config = this.buildConfig(); } async onModuleInit() { try { await this.initializeClient(); this.setupErrorHandlers(); this.logger.log('APNs Provider initialized successfully'); } catch (error) { this.logger.error('Failed to initialize APNs Provider', error); throw error; } } async onModuleDestroy() { try { await this.shutdown(); this.logger.log('APNs Provider shutdown successfully'); } catch (error) { this.logger.error('Error during APNs Provider shutdown', error); } } /** * 构建APNs配置 */ private buildConfig(): ApnsConfig { const teamId = this.configService.get('APNS_TEAM_ID'); const keyId = this.configService.get('APNS_KEY_ID'); const keyPath = this.configService.get('APNS_KEY_PATH'); const bundleId = this.configService.get('APNS_BUNDLE_ID'); const environment = this.configService.get('APNS_ENVIRONMENT', 'sandbox'); if (!teamId || !keyId || !keyPath || !bundleId) { throw new Error('Missing required APNs configuration'); } let signingKey: string | Buffer; try { // 尝试读取密钥文件 if (fs.existsSync(keyPath)) { signingKey = fs.readFileSync(keyPath); } else { // 如果是直接的内容而不是文件路径 signingKey = keyPath; } } catch (error) { this.logger.error(`Failed to read APNs key file: ${keyPath}`, error); throw new Error(`Invalid APNs key file: ${keyPath}`); } return { team: teamId, keyId, signingKey, defaultTopic: bundleId, // production: environment === 'production', }; } /** * 初始化APNs客户端 */ private async initializeClient(): Promise { try { this.logger.log(`Initializing APNs Client config: ${JSON.stringify(this.config)}`); this.client = new ApnsClient(this.config); this.logger.log(`APNs Client initialized for ${this.config.production ? 'Production' : 'Sandbox'} environment`); } catch (error) { this.logger.error('Failed to initialize APNs Client', error); throw error; } } /** * 设置错误处理器 */ private setupErrorHandlers(): void { // 监听特定错误 this.client.on(Errors.badDeviceToken, (err) => { this.logger.error(`Bad device token: ${err}`, err.reason); }); this.client.on(Errors.unregistered, (err) => { this.logger.error(`Device unregistered: ${err.deviceToken}`, err.reason); }); this.client.on(Errors.topicDisallowed, (err) => { this.logger.error(`Topic disallowed: ${err.deviceToken}`, err.reason); }); // 监听所有错误 this.client.on(Errors.error, (err) => { this.logger.error(`APNs error for device ${err.deviceToken}: ${err.reason}`, err); }); } /** * 发送单个通知 */ async send(notification: Notification, deviceTokens: string[]): Promise { const results: SendResult = { sent: [], failed: [] }; try { for (const deviceToken of deviceTokens) { try { // 为每个设备令牌创建新的通知实例 const deviceNotification = this.createDeviceNotification(notification, deviceToken); this.logger.log(`Sending notification to device this.client.send deviceNotification ${JSON.stringify(deviceNotification, null, 2)}`); await this.client.send(deviceNotification); results.sent.push(deviceToken); } catch (error) { results.failed.push({ device: deviceToken, error: error as Error }); } } this.logResults(results); return results; } catch (error) { this.logger.error('Error sending notification', error); throw error; } } /** * 批量发送通知 */ async sendBatch(notifications: Notification[], deviceTokens: string[]): Promise { const results: SendResult = { sent: [], failed: [] }; try { this.logger.debug(`Sending ${notifications.length} notifications to ${deviceTokens.length} devices`); const deviceNotifications: Notification[] = []; for (const notification of notifications) { for (const deviceToken of deviceTokens) { deviceNotifications.push(this.createDeviceNotification(notification, deviceToken)); } } const sendResults = await this.client.sendMany(deviceNotifications); // 处理 sendMany 的结果 sendResults.forEach((result, index) => { const deviceIndex = index % deviceTokens.length; const deviceToken = deviceTokens[deviceIndex]; if (result && typeof result === 'object' && 'error' in result) { results.failed.push({ device: deviceToken, error: (result as any).error }); } else { results.sent.push(deviceToken); } }); this.logResults(results); return results; } catch (error) { this.logger.error('Error sending batch notifications', error); throw error; } } /** * 管理推送通道 */ async manageChannels(notification: Notification, bundleId: string, action: string): Promise { try { this.logger.debug(`Managing channels for bundle ${bundleId} with action ${action}`); // apns2 库没有直接的 manageChannels 方法,这里需要实现自定义逻辑 // 或者使用原始的 HTTP 请求来管理通道 this.logger.warn(`Channel management not directly supported in apns2 library. Action: ${action}`); return { message: 'Channel management not implemented in apns2 library' }; } catch (error) { this.logger.error('Error managing channels', error); throw error; } } /** * 广播实时活动通知 */ async broadcast(notification: Notification, bundleId: string): Promise { try { this.logger.debug(`Broadcasting to bundle ${bundleId}`); // apns2 库没有直接的 broadcast 方法,这里需要实现自定义逻辑 // 或者使用原始的 HTTP 请求来广播 this.logger.warn(`Broadcast not directly supported in apns2 library. Bundle: ${bundleId}`); return { message: 'Broadcast not implemented in apns2 library' }; } catch (error) { this.logger.error('Error broadcasting', error); throw error; } } /** * 创建标准通知 */ createNotification(options: ApnsNotificationOptions): Notification { // 构建通知选项 const notificationOptions: any = {}; // 设置 APS 属性 const aps: any = {}; if (options.badge !== undefined) { notificationOptions.badge = options.badge; } if (options.sound) { notificationOptions.sound = options.sound; } if (options.contentAvailable) { notificationOptions.contentAvailable = true; } if (options.mutableContent) { notificationOptions.mutableContent = true; } if (options.priority) { notificationOptions.priority = options.priority; } if (options.pushType) { notificationOptions.type = options.pushType; } // 添加自定义数据 if (options.data) { notificationOptions.data = options.data; } // 创建通知对象,但不指定设备令牌(将在发送时设置) return new Notification('', notificationOptions); } /** * 创建基本通知 */ createBasicNotification(deviceToken: string, title: string, body?: string, options?: Partial): Notification { // 构建通知选项 const notificationOptions: any = { alert: { title, body: body || '' } }; if (options?.badge !== undefined) { notificationOptions.badge = options.badge; } if (options?.sound) { notificationOptions.sound = options.sound; } if (options?.contentAvailable) { notificationOptions.contentAvailable = true; } if (options?.mutableContent) { notificationOptions.mutableContent = true; } if (options?.priority) { notificationOptions.priority = options.priority; } if (options?.pushType) { notificationOptions.type = options.pushType; } // 添加自定义数据 if (options?.data) { notificationOptions.data = options.data; } return new Notification(deviceToken, notificationOptions); } /** * 创建静默通知 */ createSilentNotification(deviceToken: string): SilentNotification { return new SilentNotification(deviceToken); } /** * 为特定设备创建通知实例 */ private createDeviceNotification(notification: Notification, deviceToken: string): Notification { // 创建新的通知实例,使用相同的选项但不同的设备令牌 return new Notification(deviceToken, { alert: notification.options.alert, }); } /** * 记录推送结果 */ private logResults(results: SendResult): void { const { sent, failed } = results; this.logger.log(`Push results: ${sent.length} sent, ${failed.length} failed`); if (failed.length > 0) { failed.forEach((failure) => { if (failure.error) { this.logger.error(`Push error for device ${failure.device}: ${failure.error.message}`); } else { this.logger.warn(`Push rejected for device ${failure.device}: ${failure.status} - ${JSON.stringify(failure.response)}`); } }); } } /** * 关闭连接 */ async shutdown(): Promise { try { if (this.client) { await this.client.close(); } this.logger.log('APNs Provider connections closed'); } catch (error) { this.logger.error('Error closing APNs Provider connections', error); } } /** * 获取Provider状态 */ getStatus(): { connected: boolean; environment: string } { return { connected: !!this.client, environment: this.config.production ? 'production' : 'sandbox', }; } }