feat: 支持 push
This commit is contained in:
301
src/push-notifications/apns.provider.ts
Normal file
301
src/push-notifications/apns.provider.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as apn from '@parse/node-apn';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ApnsConfig, ApnsNotificationOptions } from './interfaces/apns-config.interface';
|
||||
|
||||
@Injectable()
|
||||
export class ApnsProvider implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(ApnsProvider.name);
|
||||
private provider: apn.Provider;
|
||||
private multiProvider: apn.MultiProvider;
|
||||
private config: ApnsConfig;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.config = this.buildConfig();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
await this.initializeProvider();
|
||||
this.logger.log('APNs Provider initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize APNs Provider', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
try {
|
||||
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 keyId = this.configService.get<string>('APNS_KEY_ID');
|
||||
const teamId = this.configService.get<string>('APNS_TEAM_ID');
|
||||
const keyPath = this.configService.get<string>('APNS_KEY_PATH');
|
||||
const bundleId = this.configService.get<string>('APNS_BUNDLE_ID');
|
||||
const environment = this.configService.get<string>('APNS_ENVIRONMENT', 'sandbox');
|
||||
const clientCount = this.configService.get<number>('APNS_CLIENT_COUNT', 2);
|
||||
|
||||
if (!keyId || !teamId || !keyPath || !bundleId) {
|
||||
throw new Error('Missing required APNs configuration');
|
||||
}
|
||||
|
||||
let key: string | Buffer;
|
||||
try {
|
||||
// 尝试读取密钥文件
|
||||
if (fs.existsSync(keyPath)) {
|
||||
key = fs.readFileSync(keyPath);
|
||||
} else {
|
||||
// 如果是直接的内容而不是文件路径
|
||||
key = keyPath;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to read APNs key file: ${keyPath}`, error);
|
||||
throw new Error(`Invalid APNs key file: ${keyPath}`);
|
||||
}
|
||||
|
||||
return {
|
||||
token: {
|
||||
key,
|
||||
keyId,
|
||||
teamId,
|
||||
},
|
||||
production: environment === 'production',
|
||||
clientCount,
|
||||
connectionRetryLimit: this.configService.get<number>('APNS_CONNECTION_RETRY_LIMIT', 3),
|
||||
heartBeat: this.configService.get<number>('APNS_HEARTBEAT', 60000),
|
||||
requestTimeout: this.configService.get<number>('APNS_REQUEST_TIMEOUT', 5000),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化APNs连接
|
||||
*/
|
||||
private async initializeProvider(): Promise<void> {
|
||||
try {
|
||||
// 创建单个Provider
|
||||
this.provider = new apn.Provider(this.config);
|
||||
|
||||
// 创建多Provider连接池
|
||||
this.multiProvider = new apn.MultiProvider(this.config);
|
||||
|
||||
this.logger.log(`APNs Provider initialized with ${this.config.clientCount} clients`);
|
||||
this.logger.log(`Environment: ${this.config.production ? 'Production' : 'Sandbox'}`);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize APNs Provider', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单个通知
|
||||
*/
|
||||
async send(notification: apn.Notification, deviceTokens: string[]): Promise<apn.Results> {
|
||||
try {
|
||||
this.logger.debug(`Sending notification to ${deviceTokens.length} devices`);
|
||||
|
||||
const results = await this.provider.send(notification, deviceTokens);
|
||||
|
||||
this.logResults(results);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error('Error sending notification', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送通知
|
||||
*/
|
||||
async sendBatch(notifications: apn.Notification[], deviceTokens: string[]): Promise<apn.Results> {
|
||||
try {
|
||||
this.logger.debug(`Sending ${notifications.length} notifications to ${deviceTokens.length} devices`);
|
||||
|
||||
const results = await this.multiProvider.send(notifications, deviceTokens);
|
||||
|
||||
this.logResults(results);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error('Error sending batch notifications', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理推送通道
|
||||
*/
|
||||
async manageChannels(notification: apn.Notification, bundleId: string, action: string): Promise<any> {
|
||||
try {
|
||||
this.logger.debug(`Managing channels for bundle ${bundleId} with action ${action}`);
|
||||
|
||||
const results = await this.provider.manageChannels(notification, bundleId, action);
|
||||
|
||||
this.logger.log(`Channel management completed: ${JSON.stringify(results)}`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error('Error managing channels', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播实时活动通知
|
||||
*/
|
||||
async broadcast(notification: apn.Notification, bundleId: string): Promise<any> {
|
||||
try {
|
||||
this.logger.debug(`Broadcasting to bundle ${bundleId}`);
|
||||
|
||||
const results = await this.provider.broadcast(notification, bundleId);
|
||||
|
||||
this.logger.log(`Broadcast completed: ${JSON.stringify(results)}`);
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.logger.error('Error broadcasting', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标准通知
|
||||
*/
|
||||
createNotification(options: {
|
||||
title?: string;
|
||||
body?: string;
|
||||
payload?: any;
|
||||
pushType?: string;
|
||||
priority?: number;
|
||||
expiry?: number;
|
||||
collapseId?: string;
|
||||
topic?: string;
|
||||
sound?: string;
|
||||
badge?: number;
|
||||
mutableContent?: boolean;
|
||||
contentAvailable?: boolean;
|
||||
}): apn.Notification {
|
||||
const notification = new apn.Notification();
|
||||
|
||||
// 设置基本内容
|
||||
if (options.title) {
|
||||
notification.title = options.title;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
notification.body = options.body;
|
||||
}
|
||||
|
||||
// 设置自定义负载
|
||||
if (options.payload) {
|
||||
notification.payload = options.payload;
|
||||
}
|
||||
|
||||
// 设置推送类型
|
||||
if (options.pushType) {
|
||||
notification.pushType = options.pushType;
|
||||
}
|
||||
|
||||
// 设置优先级
|
||||
if (options.priority) {
|
||||
notification.priority = options.priority;
|
||||
}
|
||||
|
||||
// 设置过期时间
|
||||
if (options.expiry) {
|
||||
notification.expiry = options.expiry;
|
||||
}
|
||||
|
||||
// 设置折叠ID
|
||||
if (options.collapseId) {
|
||||
notification.collapseId = options.collapseId;
|
||||
}
|
||||
|
||||
// 设置主题
|
||||
if (options.topic) {
|
||||
notification.topic = options.topic;
|
||||
}
|
||||
|
||||
// 设置声音
|
||||
if (options.sound) {
|
||||
notification.sound = options.sound;
|
||||
}
|
||||
|
||||
// 设置徽章
|
||||
if (options.badge) {
|
||||
notification.badge = options.badge;
|
||||
}
|
||||
|
||||
// 设置可变内容
|
||||
if (options.mutableContent) {
|
||||
notification.mutableContent = 1;
|
||||
}
|
||||
|
||||
// 设置静默推送
|
||||
if (options.contentAvailable) {
|
||||
notification.contentAvailable = 1;
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录推送结果
|
||||
*/
|
||||
private logResults(results: apn.Results): 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)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
shutdown(): void {
|
||||
try {
|
||||
if (this.provider) {
|
||||
this.provider.shutdown();
|
||||
}
|
||||
|
||||
if (this.multiProvider) {
|
||||
this.multiProvider.shutdown();
|
||||
}
|
||||
|
||||
this.logger.log('APNs Provider connections closed');
|
||||
} catch (error) {
|
||||
this.logger.error('Error closing APNs Provider connections', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Provider状态
|
||||
*/
|
||||
getStatus(): { connected: boolean; clientCount: number; environment: string } {
|
||||
return {
|
||||
connected: !!(this.provider || this.multiProvider),
|
||||
clientCount: this.config.clientCount || 1,
|
||||
environment: this.config.production ? 'production' : 'sandbox',
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user