meshnet.ts (3700B)
1 import { nfc_message_json, nfc_record_data_text, nfc_record_json } from "./records.js"; 2 import type { NfcMessageInput, NfcReadPayload, NfcRecord, NfcRecordInput } from "./types.js"; 3 4 export type NfcMeshnetProfile = { 5 display_name: string; 6 device_label: string; 7 }; 8 9 export type NfcMeshnetPacketProfile = { 10 type: "meshnet.profile.v1"; 11 profile: NfcMeshnetProfile; 12 timestamp_ms: number; 13 }; 14 15 export type NfcMeshnetPacketChat = { 16 type: "meshnet.chat.v1"; 17 message_id: string; 18 profile: NfcMeshnetProfile; 19 text: string; 20 timestamp_ms: number; 21 }; 22 23 export type NfcMeshnetPacket = NfcMeshnetPacketProfile | NfcMeshnetPacketChat; 24 25 export const NFC_MESHNET_MEDIA_TYPE = "application/vnd.radroots.meshnet+json"; 26 27 const is_record = (value: unknown): value is Record<string, unknown> => 28 typeof value === "object" && value !== null && !Array.isArray(value); 29 30 const is_string = (value: unknown): value is string => typeof value === "string"; 31 32 const is_number = (value: unknown): value is number => 33 typeof value === "number" && Number.isFinite(value); 34 35 const is_profile = (value: unknown): value is NfcMeshnetProfile => { 36 if (!is_record(value)) return false; 37 return is_string(value.display_name) && is_string(value.device_label); 38 }; 39 40 const is_packet_profile = (value: unknown): value is NfcMeshnetPacketProfile => { 41 if (!is_record(value)) return false; 42 if (value.type !== "meshnet.profile.v1") return false; 43 if (!is_profile(value.profile)) return false; 44 if (!is_number(value.timestamp_ms)) return false; 45 return true; 46 }; 47 48 const is_packet_chat = (value: unknown): value is NfcMeshnetPacketChat => { 49 if (!is_record(value)) return false; 50 if (value.type !== "meshnet.chat.v1") return false; 51 if (!is_string(value.message_id)) return false; 52 if (!is_profile(value.profile)) return false; 53 if (!is_string(value.text)) return false; 54 if (!is_number(value.timestamp_ms)) return false; 55 return true; 56 }; 57 58 export const nfc_meshnet_packet_profile = ( 59 profile: NfcMeshnetProfile, 60 timestamp_ms: number, 61 ): NfcMeshnetPacketProfile => { 62 return { 63 type: "meshnet.profile.v1", 64 profile, 65 timestamp_ms, 66 }; 67 }; 68 69 export const nfc_meshnet_packet_chat = ( 70 profile: NfcMeshnetProfile, 71 text: string, 72 message_id: string, 73 timestamp_ms: number, 74 ): NfcMeshnetPacketChat => { 75 return { 76 type: "meshnet.chat.v1", 77 message_id, 78 profile, 79 text, 80 timestamp_ms, 81 }; 82 }; 83 84 export const nfc_meshnet_record_from_packet = ( 85 packet: NfcMeshnetPacket, 86 ): NfcRecordInput => { 87 return nfc_record_json(packet, { media_type: NFC_MESHNET_MEDIA_TYPE }); 88 }; 89 90 export const nfc_meshnet_message_from_packet = ( 91 packet: NfcMeshnetPacket, 92 ): NfcMessageInput => { 93 return nfc_message_json(packet, { media_type: NFC_MESHNET_MEDIA_TYPE }); 94 }; 95 96 export const nfc_meshnet_packet_from_record = ( 97 record: NfcRecord, 98 ): NfcMeshnetPacket | undefined => { 99 if (record.record_type !== "mime") return; 100 if (record.media_type !== NFC_MESHNET_MEDIA_TYPE) return; 101 const text = nfc_record_data_text(record); 102 if (!text) return; 103 let parsed: unknown; 104 try { 105 parsed = JSON.parse(text); 106 } catch { 107 return; 108 } 109 if (is_packet_profile(parsed)) return parsed; 110 if (is_packet_chat(parsed)) return parsed; 111 }; 112 113 export const nfc_meshnet_packets_from_payload = ( 114 payload: NfcReadPayload, 115 ): NfcMeshnetPacket[] => { 116 const packets: NfcMeshnetPacket[] = []; 117 for (const record of payload.message.records) { 118 const packet = nfc_meshnet_packet_from_record(record); 119 if (packet) packets.push(packet); 120 } 121 return packets; 122 };