mirror of
https://github.com/vernu/textbee.git
synced 2026-02-19 23:26:14 -05:00
feat(api): create heart beat endpoint
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
||||
SendBulkSMSInputDTO,
|
||||
SendSMSInputDTO,
|
||||
UpdateSMSStatusDTO,
|
||||
HeartbeatInputDTO,
|
||||
HeartbeatResponseDTO,
|
||||
} from './gateway.dto'
|
||||
import { GatewayService } from './gateway.service'
|
||||
import { CanModifyDevice } from './guards/can-modify-device.guard'
|
||||
@@ -70,6 +72,18 @@ export class GatewayController {
|
||||
return { data }
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Device heartbeat' })
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Post('/devices/:id/heartbeat')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async heartbeat(
|
||||
@Param('id') deviceId: string,
|
||||
@Body() input: HeartbeatInputDTO,
|
||||
): Promise<HeartbeatResponseDTO> {
|
||||
const data = await this.gatewayService.heartbeat(deviceId, input)
|
||||
return data
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Delete device' })
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Delete('/devices/:id')
|
||||
|
||||
@@ -299,3 +299,134 @@ export class UpdateSMSStatusDTO {
|
||||
})
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export class HeartbeatInputDTO {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'FCM token for push notifications',
|
||||
})
|
||||
fcmToken?: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Battery percentage (0-100)',
|
||||
})
|
||||
batteryPercentage?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
description: 'Whether device is currently charging',
|
||||
})
|
||||
isCharging?: boolean
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'Network type',
|
||||
enum: ['wifi', 'cellular', 'none'],
|
||||
})
|
||||
networkType?: 'wifi' | 'cellular' | 'none'
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'App version name',
|
||||
})
|
||||
appVersionName?: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'App version code',
|
||||
})
|
||||
appVersionCode?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Device uptime in milliseconds since boot',
|
||||
})
|
||||
deviceUptimeMillis?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Free memory in bytes',
|
||||
})
|
||||
memoryFreeBytes?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Total memory in bytes',
|
||||
})
|
||||
memoryTotalBytes?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Max memory in bytes',
|
||||
})
|
||||
memoryMaxBytes?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Available storage in bytes',
|
||||
})
|
||||
storageAvailableBytes?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
required: false,
|
||||
description: 'Total storage in bytes',
|
||||
})
|
||||
storageTotalBytes?: number
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'Device timezone (e.g., "America/New_York")',
|
||||
})
|
||||
timezone?: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
description: 'Device locale (e.g., "en_US")',
|
||||
})
|
||||
locale?: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
description: 'Whether receive SMS feature is enabled',
|
||||
})
|
||||
receiveSMSEnabled?: boolean
|
||||
}
|
||||
|
||||
export class HeartbeatResponseDTO {
|
||||
@ApiProperty({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
description: 'Whether the heartbeat was successful',
|
||||
})
|
||||
success: boolean
|
||||
|
||||
@ApiProperty({
|
||||
type: Boolean,
|
||||
required: true,
|
||||
description: 'Whether the FCM token was updated',
|
||||
})
|
||||
fcmTokenUpdated: boolean
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
required: true,
|
||||
description: 'Server timestamp of the heartbeat',
|
||||
})
|
||||
lastHeartbeat: Date
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
SendBulkSMSInputDTO,
|
||||
SendSMSInputDTO,
|
||||
UpdateSMSStatusDTO,
|
||||
HeartbeatInputDTO,
|
||||
HeartbeatResponseDTO,
|
||||
} from './gateway.dto'
|
||||
import { User } from '../users/schemas/user.schema'
|
||||
import { AuthService } from '../auth/auth.service'
|
||||
@@ -917,4 +919,129 @@ const updatedSms = await this.smsModel.findByIdAndUpdate(
|
||||
messages: smsMessages
|
||||
};
|
||||
}
|
||||
|
||||
async heartbeat(
|
||||
deviceId: string,
|
||||
input: HeartbeatInputDTO,
|
||||
): Promise<HeartbeatResponseDTO> {
|
||||
const device = await this.deviceModel.findById(deviceId)
|
||||
|
||||
if (!device) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Device not found',
|
||||
},
|
||||
HttpStatus.NOT_FOUND,
|
||||
)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const updateData: any = {
|
||||
lastHeartbeat: now,
|
||||
}
|
||||
|
||||
let fcmTokenUpdated = false
|
||||
|
||||
// Update FCM token if provided and different
|
||||
if (input.fcmToken && input.fcmToken !== device.fcmToken) {
|
||||
updateData.fcmToken = input.fcmToken
|
||||
fcmTokenUpdated = true
|
||||
}
|
||||
|
||||
// Update receiveSMSEnabled if provided and different
|
||||
if (
|
||||
input.receiveSMSEnabled !== undefined &&
|
||||
input.receiveSMSEnabled !== device.receiveSMSEnabled
|
||||
) {
|
||||
updateData.receiveSMSEnabled = input.receiveSMSEnabled
|
||||
}
|
||||
|
||||
// Update batteryInfo if provided
|
||||
if (input.batteryPercentage !== undefined || input.isCharging !== undefined) {
|
||||
updateData.batteryInfo = {
|
||||
...(device.batteryInfo || {}),
|
||||
...(input.batteryPercentage !== undefined && { percentage: input.batteryPercentage }),
|
||||
...(input.isCharging !== undefined && { isCharging: input.isCharging }),
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update networkInfo if provided
|
||||
if (input.networkType !== undefined) {
|
||||
updateData.networkInfo = {
|
||||
...(device.networkInfo || {}),
|
||||
type: input.networkType,
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update appVersionInfo if provided
|
||||
if (input.appVersionName !== undefined || input.appVersionCode !== undefined) {
|
||||
updateData.appVersionInfo = {
|
||||
...(device.appVersionInfo || {}),
|
||||
...(input.appVersionName !== undefined && { versionName: input.appVersionName }),
|
||||
...(input.appVersionCode !== undefined && { versionCode: input.appVersionCode }),
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update deviceUptimeInfo if provided
|
||||
if (input.deviceUptimeMillis !== undefined) {
|
||||
updateData.deviceUptimeInfo = {
|
||||
...(device.deviceUptimeInfo || {}),
|
||||
uptimeMillis: input.deviceUptimeMillis,
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update memoryInfo if any memory field provided
|
||||
if (
|
||||
input.memoryFreeBytes !== undefined ||
|
||||
input.memoryTotalBytes !== undefined ||
|
||||
input.memoryMaxBytes !== undefined
|
||||
) {
|
||||
updateData.memoryInfo = {
|
||||
...(device.memoryInfo || {}),
|
||||
...(input.memoryFreeBytes !== undefined && { freeBytes: input.memoryFreeBytes }),
|
||||
...(input.memoryTotalBytes !== undefined && { totalBytes: input.memoryTotalBytes }),
|
||||
...(input.memoryMaxBytes !== undefined && { maxBytes: input.memoryMaxBytes }),
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update storageInfo if any storage field provided
|
||||
if (
|
||||
input.storageAvailableBytes !== undefined ||
|
||||
input.storageTotalBytes !== undefined
|
||||
) {
|
||||
updateData.storageInfo = {
|
||||
...(device.storageInfo || {}),
|
||||
...(input.storageAvailableBytes !== undefined && { availableBytes: input.storageAvailableBytes }),
|
||||
...(input.storageTotalBytes !== undefined && { totalBytes: input.storageTotalBytes }),
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update systemInfo if timezone or locale provided
|
||||
if (input.timezone !== undefined || input.locale !== undefined) {
|
||||
updateData.systemInfo = {
|
||||
...(device.systemInfo || {}),
|
||||
...(input.timezone !== undefined && { timezone: input.timezone }),
|
||||
...(input.locale !== undefined && { locale: input.locale }),
|
||||
lastUpdated: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update device with all changes
|
||||
await this.deviceModel.findByIdAndUpdate(deviceId, {
|
||||
$set: updateData,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fcmTokenUpdated,
|
||||
lastHeartbeat: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,107 @@ export class Device {
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
receivedSMSCount: number
|
||||
|
||||
@Prop({ type: Boolean, default: true })
|
||||
heartbeatEnabled: boolean
|
||||
|
||||
@Prop({ type: Number, default: 30 })
|
||||
heartbeatIntervalMinutes: number
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
receiveSMSEnabled: boolean
|
||||
|
||||
@Prop({ type: Date })
|
||||
lastHeartbeat: Date
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
percentage: Number,
|
||||
isCharging: Boolean,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
batteryInfo: {
|
||||
percentage?: number
|
||||
isCharging?: boolean
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
type: String,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
networkInfo: {
|
||||
type?: 'wifi' | 'cellular' | 'none'
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
versionName: String,
|
||||
versionCode: Number,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
appVersionInfo: {
|
||||
versionName?: string
|
||||
versionCode?: number
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
uptimeMillis: Number,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
deviceUptimeInfo: {
|
||||
uptimeMillis?: number
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
freeBytes: Number,
|
||||
totalBytes: Number,
|
||||
maxBytes: Number,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
memoryInfo: {
|
||||
freeBytes?: number
|
||||
totalBytes?: number
|
||||
maxBytes?: number
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
availableBytes: Number,
|
||||
totalBytes: Number,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
storageInfo: {
|
||||
availableBytes?: number
|
||||
totalBytes?: number
|
||||
lastUpdated?: Date
|
||||
}
|
||||
|
||||
@Prop({
|
||||
type: {
|
||||
timezone: String,
|
||||
locale: String,
|
||||
lastUpdated: Date,
|
||||
},
|
||||
})
|
||||
systemInfo: {
|
||||
timezone?: string
|
||||
locale?: string
|
||||
lastUpdated?: Date
|
||||
}
|
||||
}
|
||||
|
||||
export const DeviceSchema = SchemaFactory.createForClass(Device)
|
||||
|
||||
Reference in New Issue
Block a user