270 lines
9.2 KiB
TypeScript
270 lines
9.2 KiB
TypeScript
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,
|
|
);
|
|
}
|