import type { ListMobileDeviceMediaFilters, ListMobileDeviceSMSFilters, ListMobileDevicesFilters, MobileMediaAssetInput, MobilePagination, MobileSMSInput, PingMobileDevice, RegisterMobileDevice, } from "./data"; import type { FlowExecCtx } from "@core/flow.execution.context"; import { traceResultAsync } from "@core/observability"; import { MobileRepository } from "./repository"; import { settings } from "@core/settings"; import { mobileErrors } from "./errors"; import { getFileController, type FileController } from "@domains/files/controller"; import { errAsync, okAsync } from "neverthrow"; import { db } from "@pkg/db"; export class MobileController { constructor( private mobileRepo: MobileRepository, private fileController: FileController, private defaultAdminEmail?: string, ) {} registerDevice(fctx: FlowExecCtx, payload: RegisterMobileDevice) { return traceResultAsync({ name: "mobile.register", fctx, attributes: { "app.mobile.external_device_id": payload.externalDeviceId, }, fn: () => this.mobileRepo .findAdminOwnerId(fctx, this.defaultAdminEmail) .andThen((ownerUserId) => this.mobileRepo.upsertDevice( fctx, payload, ownerUserId, ), ), }); } pingByExternalDeviceId( fctx: FlowExecCtx, externalDeviceId: string, payload?: PingMobileDevice, ) { return traceResultAsync({ name: "mobile.ping", fctx, attributes: { "app.mobile.external_device_id": externalDeviceId }, fn: () => this.mobileRepo .getDeviceByExternalId(fctx, externalDeviceId) .andThen((device) => { const pingAt = payload?.pingAt ? new Date(payload.pingAt as Date | string) : new Date(); if (Number.isNaN(pingAt.getTime())) { return errAsync( mobileErrors.invalidPayload( fctx, "pingAt must be a valid date", ), ); } return this.mobileRepo.touchDevicePing( fctx, device.id, pingAt, ); }), }); } syncSMSByExternalDeviceId( fctx: FlowExecCtx, externalDeviceId: string, messages: MobileSMSInput[], ) { return traceResultAsync({ name: "mobile.sms.sync", fctx, attributes: { "app.mobile.external_device_id": externalDeviceId, "app.mobile.sms.received_count": messages.length, }, fn: () => this.mobileRepo .getDeviceByExternalId(fctx, externalDeviceId) .andThen((device) => this.mobileRepo .syncSMS(fctx, device.id, messages) .andThen((syncResult) => this.mobileRepo .touchDevicePing(fctx, device.id) .map(() => ({ ...syncResult, deviceId: device.id, })), ), ), }); } syncMediaByExternalDeviceId( fctx: FlowExecCtx, externalDeviceId: string, assets: MobileMediaAssetInput[], ) { return traceResultAsync({ name: "mobile.media.sync", fctx, attributes: { "app.mobile.external_device_id": externalDeviceId, "app.mobile.media.received_count": assets.length, }, fn: () => this.mobileRepo .getDeviceByExternalId(fctx, externalDeviceId) .andThen((device) => this.mobileRepo .syncMediaAssets(fctx, device.id, assets) .andThen((syncResult) => this.mobileRepo .touchDevicePing(fctx, device.id) .map(() => ({ ...syncResult, deviceId: device.id, })), ), ), }); } listDevices( fctx: FlowExecCtx, filters: ListMobileDevicesFilters, pagination: MobilePagination, ) { return traceResultAsync({ name: "mobile.devices.list", fctx, attributes: { "app.user.id": filters.ownerUserId }, fn: () => this.mobileRepo.listDevices(fctx, filters, pagination), }); } getDeviceDetail(fctx: FlowExecCtx, deviceId: number, ownerUserId: string) { return traceResultAsync({ name: "mobile.device.detail", fctx, attributes: { "app.user.id": ownerUserId, "app.mobile.device_id": deviceId, }, fn: () => this.mobileRepo.getDeviceDetail(fctx, deviceId, ownerUserId), }); } listDeviceSMS( fctx: FlowExecCtx, filters: ListMobileDeviceSMSFilters, pagination: MobilePagination, ) { return traceResultAsync({ name: "mobile.device.sms.list", fctx, attributes: { "app.mobile.device_id": filters.deviceId }, fn: () => this.mobileRepo.listDeviceSMS(fctx, filters, pagination), }); } listDeviceMedia( fctx: FlowExecCtx, filters: ListMobileDeviceMediaFilters, pagination: MobilePagination, ) { return traceResultAsync({ name: "mobile.device.media.list", fctx, attributes: { "app.mobile.device_id": filters.deviceId }, fn: () => this.mobileRepo.listDeviceMedia(fctx, filters, pagination), }); } deleteMediaAsset( fctx: FlowExecCtx, mediaAssetId: number, ownerUserId: string, ) { return traceResultAsync({ name: "mobile.media.delete", fctx, attributes: { "app.user.id": ownerUserId, "app.mobile.media_asset_id": mediaAssetId, }, fn: () => this.mobileRepo .deleteMediaAsset(fctx, mediaAssetId, ownerUserId) .andThen((fileId) => this.fileController .deleteFiles(fctx, [fileId], ownerUserId) .map(() => true), ), }); } deleteDevice(fctx: FlowExecCtx, deviceId: number, ownerUserId: string) { return traceResultAsync({ name: "mobile.device.delete", fctx, attributes: { "app.user.id": ownerUserId, "app.mobile.device_id": deviceId, }, fn: () => this.mobileRepo .deleteDevice(fctx, deviceId, ownerUserId) .andThen((result) => { const cleanup = result.fileIds.length ? this.fileController.deleteFiles( fctx, result.fileIds, ownerUserId, ) : okAsync(true); return cleanup.andThen(() => this.mobileRepo .finalizeDeleteDevice( fctx, deviceId, ownerUserId, ) .map(() => ({ deleted: true, deletedFileCount: result.fileIds.length, })), ); }), }); } resolveDeviceByExternalId(fctx: FlowExecCtx, externalDeviceId: string) { return traceResultAsync({ name: "mobile.device.resolve", fctx, attributes: { "app.mobile.external_device_id": externalDeviceId }, fn: () => this.mobileRepo.getDeviceByExternalId(fctx, externalDeviceId), }); } } export function getMobileController(): MobileController { return new MobileController( new MobileRepository(db), getFileController(), settings.defaultAdminEmail || undefined, ); }