import { MalformedRequest, parseKeptAttachment } from "./personal/parse.ts"; import type { AskChoiceOptionDocument, AskInteractionDocument, AttachmentImportRequest, ChannelEnsureRequest, DirectMessageEnsureRequest, DirectMessageOpenRequest, DirectMessagePostRequest, DirectMessageSendRequest, HistoryFetchRequest, IdentityResolveRequest, InputAttachmentDocument, MessageDeleteRequest, MessageEditRequest, MessagePostRequest, MessageSearchRequest, ProgressRequest, ReactionRequest, ReplyAttachmentDocument, ReplySendRequest, } from "./outbound-types.ts"; function requireRecord(value: unknown, context: string): Record { if (typeof value !== "object" || value === null) { throw new Error(`expected ${context} to be a JSON object`); } return value as Record; } function requireString(record: Record, field: string): string { const value = record[field]; if (typeof value !== "string" || value.trim().length === 0) { throw new Error(`missing required field ${field}`); } return value; } function optionalString(record: Record, field: string): string | undefined { const value = record[field]; return typeof value === "string" ? value : undefined; } function optionalNumber(record: Record, field: string): number | undefined { const value = record[field]; return typeof value === "number" ? value : undefined; } function optionalArray(record: Record, field: string): unknown[] { const value = record[field]; return Array.isArray(value) ? value : []; } function parseReplyAttachment(value: unknown): ReplyAttachmentDocument { const record = requireRecord(value, "reply attachment"); return { devicePath: optionalString(record, "devicePath"), filename: optionalString(record, "filename"), contentType: optionalString(record, "contentType"), sizeBytes: optionalNumber(record, "sizeBytes"), title: optionalString(record, "title"), contentBase64: optionalString(record, "contentBase64"), }; } function parseAskChoiceOption(value: unknown): AskChoiceOptionDocument { const record = requireRecord(value, "ask choice option"); return { key: requireString(record, "key"), label: requireString(record, "label"), shortLabel: optionalString(record, "shortLabel"), value: optionalString(record, "value"), }; } function parseAskInteraction(value: unknown): AskInteractionDocument { const record = requireRecord(value, "interaction"); return { interactionID: optionalString(record, "interactionID"), taskRunID: optionalString(record, "taskRunID"), kind: optionalString(record, "kind"), message: optionalString(record, "message"), question: optionalString(record, "question"), options: optionalArray(record, "options").map(parseAskChoiceOption), recommendedOptionKey: optionalString(record, "recommendedOptionKey"), selectionMode: optionalString(record, "selectionMode"), responseLanguage: optionalString(record, "responseLanguage"), targetPlatformUserID: optionalString(record, "targetPlatformUserID"), }; } export function parseReplySendRequest(value: unknown): ReplySendRequest { const record = requireRecord(value, "reply.send request"); const interactionValue = record.interaction; return { replyTargetID: requireString(record, "replyTargetID"), answeringMessageID: optionalString(record, "answeringMessageID"), message: requireString(record, "message"), taskRunID: optionalString(record, "taskRunID"), replyKind: optionalString(record, "replyKind"), rawEventID: optionalString(record, "rawEventID"), outboxID: optionalString(record, "outboxID"), attachments: optionalArray(record, "attachments").map(parseReplyAttachment), interaction: interactionValue === undefined ? undefined : parseAskInteraction(interactionValue), isError: hasFailureNotice(record.failureNotice), }; } function hasFailureNotice(value: unknown): boolean { if (typeof value !== "object" || value === null) return false; const message = (value as Record).message; return typeof message === "string" && message.trim().length > 0; } export function parseProgressRequest(value: unknown): ProgressRequest { const record = requireRecord(value, "progress request"); return { replyTargetID: requireString(record, "replyTargetID"), answeringMessageID: optionalString(record, "answeringMessageID"), }; } export function parseReactionRequest(value: unknown): ReactionRequest { const record = requireRecord(value, "reaction request"); return { conversationID: optionalString(record, "conversationID"), messageID: requireString(record, "messageID"), emojiName: requireString(record, "emojiName"), reason: optionalString(record, "reason"), }; } export function parseHistoryFetchRequest(value: unknown): HistoryFetchRequest { const record = requireRecord(value, "history.fetch request"); const request = { historyCursor: optionalString(record, "historyCursor"), threadID: optionalString(record, "threadID"), channelID: optionalString(record, "channelID"), channelName: optionalString(record, "channelName"), limit: optionalNumber(record, "limit"), direction: optionalString(record, "direction"), }; if (!request.historyCursor && !request.threadID && !request.channelID && !request.channelName) { throw new MalformedRequest("history.fetch needs a historyCursor, a threadID, a channelID, or a channelName"); } return request; } export function parseMessageEditRequest(value: unknown): MessageEditRequest { const record = requireRecord(value, "message.edit request"); return { replyTargetID: requireString(record, "replyTargetID"), messageID: requireString(record, "messageID"), message: requireString(record, "message"), requesterPubkeyHex: optionalString(record, "requesterPubkeyHex"), attachments: optionalArray(record, "attachments").map(parseReplyAttachment), }; } export function parseMessageDeleteRequest(value: unknown): MessageDeleteRequest { const record = requireRecord(value, "message_delete request"); return { replyTargetID: requireString(record, "replyTargetID"), messageID: requireString(record, "messageID"), requesterPubkeyHex: optionalString(record, "requesterPubkeyHex"), }; } export function parseMessageSearchRequest(value: unknown): MessageSearchRequest { const record = requireRecord(value, "message.search request"); const request: MessageSearchRequest = { replyTargetID: optionalString(record, "replyTargetID"), channelID: optionalString(record, "channelID"), channelName: optionalString(record, "channelName"), rootMessageID: optionalString(record, "rootMessageID"), messageIDs: optionalArray(record, "messageIDs").filter( (entry): entry is string => typeof entry === "string" && entry.trim() !== "", ), authoredBy: optionalString(record, "authoredBy"), requesterPubkeyHex: optionalString(record, "requesterPubkeyHex"), queries: optionalArray(record, "queries").filter( (entry): entry is string => typeof entry === "string" && entry.trim() !== "", ), limit: optionalNumber(record, "limit"), }; return request; } export function parseDirectMessageSendRequest(value: unknown): DirectMessageSendRequest { const record = requireRecord(value, "dm.send request"); return { userSecretHex: requireString(record, "userSecretHex"), message: optionalString(record, "message") ?? "", attachments: optionalArray(record, "attachments").map(parseReplyAttachment), channelId: optionalString(record, "channelId"), replyToRootId: optionalString(record, "replyToRootId"), counterpartPubkeyHex: optionalString(record, "counterpartPubkeyHex"), }; } export function parseDirectMessagePostRequest(value: unknown): DirectMessagePostRequest { const record = requireRecord(value, "dm.post request"); const counterpartPubkeyHex = requireString(record, "counterpartPubkeyHex").toLowerCase(); if (!/^[0-9a-f]{64}$/.test(counterpartPubkeyHex)) { throw new MalformedRequest("counterpartPubkeyHex must be 64 hex characters"); } return { counterpartPubkeyHex, message: requireString(record, "message") }; } export function parseDirectMessageEnsureRequest(value: unknown): DirectMessageEnsureRequest { const record = requireRecord(value, "dm.ensure request"); return { userSecretHex: requireString(record, "userSecretHex"), channelId: optionalString(record, "channelId"), counterpartPubkeyHex: optionalString(record, "counterpartPubkeyHex"), }; } export function parseDirectMessageOpenRequest(value: unknown): DirectMessageOpenRequest { const record = requireRecord(value, "dm.open request"); return { externalUserID: requireString(record, "externalUserID") }; } export function parseConversationsListRequest(value: unknown): { userSecretHex: string } { const record = requireRecord(value, "conversations.list request"); return { userSecretHex: requireString(record, "userSecretHex") }; } export function parsePeopleListRequest(value: unknown): { userSecretHex: string } { const record = requireRecord(value, "people.list request"); return { userSecretHex: requireString(record, "userSecretHex") }; } export function parseChannelEnsureRequest(value: unknown): ChannelEnsureRequest { const record = requireRecord(value, "channel.ensure request"); return { name: requireString(record, "name"), displayName: optionalString(record, "displayName"), description: optionalString(record, "description"), topic: optionalString(record, "topic"), }; } function parseInputAttachment(value: unknown): InputAttachmentDocument { const record = requireRecord(value, "input attachment"); return { platform: optionalString(record, "platform"), fileID: optionalString(record, "fileID"), url: optionalString(record, "url"), messageID: optionalString(record, "messageID"), filename: optionalString(record, "filename"), contentType: optionalString(record, "contentType"), sizeBytes: optionalNumber(record, "sizeBytes"), }; } export function parseMessagePostRequest(value: unknown): MessagePostRequest { const record = requireRecord(value, "message.post request"); const attachments = optionalArray(record, "attachments").map((entry) => requireRecord(entry, "attachment")); const request: MessagePostRequest = { threadID: optionalString(record, "threadID"), channelID: optionalString(record, "channelID"), channelName: optionalString(record, "channelName"), message: attachments.length > 0 ? (optionalString(record, "message") ?? "") : requireString(record, "message"), attachments: attachments.filter((entry) => !("address" in entry)).map(parseReplyAttachment), keptAttachments: attachments.filter((entry) => "address" in entry).map(parseKeptAttachment), }; if (!request.threadID && !request.channelID && !request.channelName) { throw new Error("message.post requires threadID, channelID, or channelName"); } return request; } export function parseAttachmentImportRequest(value: unknown): AttachmentImportRequest { const record = requireRecord(value, "attachments.import request"); return { messageID: requireString(record, "messageID"), targetDirectoryPath: requireString(record, "targetDirectoryPath"), inputAttachments: optionalArray(record, "inputAttachments").map(parseInputAttachment), }; } export function parseIdentityResolveRequest(value: unknown): IdentityResolveRequest { const record = requireRecord(value, "identity.resolve request"); return { senderID: requireString(record, "senderID") }; }