summaryrefslogtreecommitdiff
path: root/client/3rd-party
diff options
context:
space:
mode:
authorGravatar moon <moon@coffin.ir> 2025-09-14 13:47:32 +0330
committerGravatar moon <moon@coffin.ir> 2025-09-14 13:47:32 +0330
commitf7485f452360beae96280fd212f52c7904871784 (patch)
treec44e61fbcdc5c0b856fd07959f5440ea6b249bb9 /client/3rd-party
parentts + py clients (diff)
moved to 3rd party
Diffstat (limited to 'client/3rd-party')
-rw-r--r--client/3rd-party/client.py683
-rw-r--r--client/3rd-party/client.ts674
2 files changed, 1357 insertions, 0 deletions
diff --git a/client/3rd-party/client.py b/client/3rd-party/client.py
new file mode 100644
index 0000000..56a8faa
--- /dev/null
+++ b/client/3rd-party/client.py
@@ -0,0 +1,683 @@
+# ai generated (i don't like python)
+
+import ctypes
+import os
+import platform
+from ctypes import (
+ Structure, Union, POINTER, c_uint8, c_uint16, c_uint32, c_uint64,
+ c_bool, c_int, c_char, c_void_p, c_size_t, CFUNCTYPE, byref, cast
+)
+from typing import List, Optional, Tuple, Union as PyUnion
+from dataclasses import dataclass
+from enum import IntEnum
+import sys
+
+# Type aliases
+byte = c_uint8
+hash_t = byte * 32
+public_key = byte * 32
+private_key = byte * 32
+signature_t = byte * 64
+ipv4 = byte * 4
+ipv6 = byte * 16
+
+# Constants
+MAX_RECIPIENTS = 32
+MOTD_SIZE = 128
+DATA_SIZE = 65536
+SIGN_PUBLICKEYBYTES = 32
+SIGN_SECRETKEYBYTES = 64
+
+class ClientError(IntEnum):
+ SUCCESS = 0
+ NETWORK_FAIL = 1
+ INVALID_ADDRESS = 2
+ CONNECTION_FAILED = 3
+ SEND_FAILED = 4
+ RECV_FAILED = 5
+ SERVER_BUSY = 6
+ INVALID_RESPONSE = 7
+ DESERIALIZE_FAILED = 8
+ ALLOC_FAILED = 9
+ FILE_NOT_FOUND = 10
+ FILE_READ_FAILED = 11
+ BAD_ARGUMENT = 12
+
+# C Structure definitions
+class RelayNeighbor(Structure):
+ _fields_ = [
+ ('active', c_bool),
+ ('has_ipv4', c_bool),
+ ('has_ipv6', c_bool),
+ ('v4_addr', ipv4),
+ ('v6_addr', ipv6)
+ ]
+
+class RelayCapabilities(Structure):
+ _fields_ = [
+ ('retention_hours', c_uint16),
+ ('can_store', c_bool),
+ ('leading_zeros', byte),
+ ('accepts_plain', c_bool)
+ ]
+
+class RelayStatistics(Structure):
+ _fields_ = [
+ ('neighbors', c_uint16),
+ ('held_packets', c_uint64),
+ ('quick_connect', RelayNeighbor * 4)
+ ]
+
+class RelayInfo(Structure):
+ _fields_ = [
+ ('major_version', byte),
+ ('minor_version', byte),
+ ('motd', c_char * MOTD_SIZE),
+ ('capabilities', RelayCapabilities),
+ ('statistics', RelayStatistics)
+ ]
+
+class DataPacketContents(Structure):
+ _fields_ = [
+ ('encrypted', c_bool),
+ ('created_time_ms', c_uint64),
+ ('author', public_key),
+ ('author_signing_pub', public_key),
+ ('recipient_count', c_uint8),
+ ('recipients', public_key * MAX_RECIPIENTS),
+ ('data', byte * DATA_SIZE),
+ ('signature', signature_t)
+ ]
+
+class DataPacket(Structure):
+ _fields_ = [
+ ('nonce', c_uint32),
+ ('hash', hash_t),
+ ('contents', DataPacketContents)
+ ]
+
+class KeyPair(Structure):
+ _fields_ = [
+ ('encryption_pub', public_key),
+ ('encryption_priv', private_key),
+ ('signing_pub', byte * SIGN_PUBLICKEYBYTES),
+ ('signing_priv', byte * SIGN_SECRETKEYBYTES)
+ ]
+
+class DataPacketList(Structure):
+ _fields_ = [
+ ('packets', POINTER(DataPacket)),
+ ('count', c_uint16),
+ ('new_offset', c_uint64)
+ ]
+
+# Python dataclasses for easier usage
+@dataclass
+class PyRelayNeighbor:
+ active: bool
+ has_ipv4: bool
+ has_ipv6: bool
+ v4_addr: bytes
+ v6_addr: bytes
+
+ @classmethod
+ def from_c(cls, c_neighbor: RelayNeighbor) -> 'PyRelayNeighbor':
+ return cls(
+ active=c_neighbor.active,
+ has_ipv4=c_neighbor.has_ipv4,
+ has_ipv6=c_neighbor.has_ipv6,
+ v4_addr=bytes(c_neighbor.v4_addr),
+ v6_addr=bytes(c_neighbor.v6_addr)
+ )
+
+ def format_ip(self) -> str:
+ if self.has_ipv4:
+ return '.'.join(str(b) for b in self.v4_addr)
+ elif self.has_ipv6:
+ parts = []
+ for i in range(0, 16, 2):
+ word = (self.v6_addr[i] << 8) | self.v6_addr[i + 1]
+ parts.append(f'{word:x}')
+ return ':'.join(parts)
+ return 'N/A'
+
+@dataclass
+class PyRelayCapabilities:
+ retention_hours: int
+ can_store: bool
+ leading_zeros: int
+ accepts_plain: bool
+
+@dataclass
+class PyRelayStatistics:
+ neighbors: int
+ held_packets: int
+ quick_connect: List[PyRelayNeighbor]
+
+@dataclass
+class PyRelayInfo:
+ major_version: int
+ minor_version: int
+ motd: str
+ capabilities: PyRelayCapabilities
+ statistics: PyRelayStatistics
+
+ @classmethod
+ def from_c(cls, c_info: RelayInfo) -> 'PyRelayInfo':
+ return cls(
+ major_version=c_info.major_version,
+ minor_version=c_info.minor_version,
+ motd=c_info.motd.decode('utf-8', errors='ignore').rstrip('\x00'),
+ capabilities=PyRelayCapabilities(
+ retention_hours=c_info.capabilities.retention_hours,
+ can_store=c_info.capabilities.can_store,
+ leading_zeros=c_info.capabilities.leading_zeros,
+ accepts_plain=c_info.capabilities.accepts_plain
+ ),
+ statistics=PyRelayStatistics(
+ neighbors=c_info.statistics.neighbors,
+ held_packets=c_info.statistics.held_packets,
+ quick_connect=[PyRelayNeighbor.from_c(n) for n in c_info.statistics.quick_connect]
+ )
+ )
+
+@dataclass
+class PyDataPacketContents:
+ encrypted: bool
+ created_time_ms: int
+ author: bytes
+ author_signing_pub: bytes
+ recipient_count: int
+ recipients: List[bytes]
+ data: bytes
+ signature: bytes
+
+@dataclass
+class PyDataPacket:
+ nonce: int
+ hash: bytes
+ contents: PyDataPacketContents
+
+ @classmethod
+ def from_c(cls, c_packet: DataPacket) -> 'PyDataPacket':
+ recipients = []
+ for i in range(c_packet.contents.recipient_count):
+ recipients.append(bytes(c_packet.contents.recipients[i]))
+
+ return cls(
+ nonce=c_packet.nonce,
+ hash=bytes(c_packet.hash),
+ contents=PyDataPacketContents(
+ encrypted=c_packet.contents.encrypted,
+ created_time_ms=c_packet.contents.created_time_ms,
+ author=bytes(c_packet.contents.author),
+ author_signing_pub=bytes(c_packet.contents.author_signing_pub),
+ recipient_count=c_packet.contents.recipient_count,
+ recipients=recipients,
+ data=bytes(c_packet.contents.data),
+ signature=bytes(c_packet.contents.signature)
+ )
+ )
+
+ def to_c(self) -> DataPacket:
+ c_packet = DataPacket()
+ c_packet.nonce = self.nonce
+
+ for i, b in enumerate(self.hash[:32]):
+ c_packet.hash[i] = b
+
+ c_packet.contents.encrypted = self.contents.encrypted
+ c_packet.contents.created_time_ms = self.contents.created_time_ms
+ c_packet.contents.recipient_count = self.contents.recipient_count
+
+ for i, b in enumerate(self.contents.author[:32]):
+ c_packet.contents.author[i] = b
+
+ for i, b in enumerate(self.contents.author_signing_pub[:32]):
+ c_packet.contents.author_signing_pub[i] = b
+
+ for i, recipient in enumerate(self.contents.recipients[:MAX_RECIPIENTS]):
+ for j, b in enumerate(recipient[:32]):
+ c_packet.contents.recipients[i][j] = b
+
+ for i, b in enumerate(self.contents.data[:DATA_SIZE]):
+ c_packet.contents.data[i] = b
+
+ for i, b in enumerate(self.contents.signature[:64]):
+ c_packet.contents.signature[i] = b
+
+ return c_packet
+
+@dataclass
+class PyKeyPair:
+ encryption_pub: bytes
+ encryption_priv: bytes
+ signing_pub: bytes
+ signing_priv: bytes
+
+ @classmethod
+ def from_c(cls, c_keypair: KeyPair) -> 'PyKeyPair':
+ return cls(
+ encryption_pub=bytes(c_keypair.encryption_pub),
+ encryption_priv=bytes(c_keypair.encryption_priv),
+ signing_pub=bytes(c_keypair.signing_pub),
+ signing_priv=bytes(c_keypair.signing_priv)
+ )
+
+ def to_c(self) -> KeyPair:
+ c_keypair = KeyPair()
+
+ for i, b in enumerate(self.encryption_pub[:32]):
+ c_keypair.encryption_pub[i] = b
+ for i, b in enumerate(self.encryption_priv[:32]):
+ c_keypair.encryption_priv[i] = b
+ for i, b in enumerate(self.signing_pub[:32]):
+ c_keypair.signing_pub[i] = b
+ for i, b in enumerate(self.signing_priv[:64]):
+ c_keypair.signing_priv[i] = b
+
+ return c_keypair
+
+@dataclass
+class PyDataPacketList:
+ packets: List[PyDataPacket]
+ count: int
+ new_offset: int
+
+class MoonpasteClientError(Exception):
+ '''Exception raised for moonpaste client errors'''
+ def __init__(self, error_code: ClientError, message: str = ''):
+ self.error_code = error_code
+ self.message = message or f'Client error: {error_code.name}'
+ super().__init__(self.message)
+
+def find_library() -> str:
+ '''Find the moonpaste client library'''
+ system = platform.system().lower()
+
+ if system == 'linux':
+ lib_names = ['libmpclient.so']
+ elif system == 'darwin':
+ lib_names = ['libmpclient.dylib']
+ elif system == 'windows':
+ lib_names = ['libmpclient.dll']
+ else:
+ lib_names = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll']
+
+ search_paths = [
+ './',
+ './lib/',
+ '../lib/',
+ '/usr/local/lib/',
+ '/usr/lib/',
+ os.environ.get('MOONPASTE_LIB_PATH', '')
+ ]
+
+ search_paths = [p for p in search_paths if p]
+
+ for search_path in search_paths:
+ for lib_name in lib_names:
+ full_path = os.path.join(search_path, lib_name)
+ if os.path.exists(full_path):
+ return full_path
+
+ raise FileNotFoundError(f'Could not find moonpaste client library. Searched for: {lib_names}')
+
+class MoonpasteFFI:
+ '''Low-level FFI wrapper for the moonpaste client library'''
+
+ def __init__(self):
+ lib_path = find_library()
+ self.lib = ctypes.CDLL(lib_path)
+ self._setup_function_signatures()
+
+ def _setup_function_signatures(self):
+ '''Setup C function signatures'''
+
+ # mp_get_server_info
+ self.lib.mp_get_server_info.argtypes = [
+ ctypes.c_char_p, # ip
+ c_int, # port
+ POINTER(RelayInfo) # out_info
+ ]
+ self.lib.mp_get_server_info.restype = c_int
+
+ # mp_get_server_neighbors
+ self.lib.mp_get_server_neighbors.argtypes = [
+ ctypes.c_char_p, # ip
+ c_int, # port
+ byte, # count
+ POINTER(POINTER(RelayNeighbor)), # out_neighbors
+ POINTER(byte) # out_count
+ ]
+ self.lib.mp_get_server_neighbors.restype = c_int
+
+ # mp_push_packet
+ self.lib.mp_push_packet.argtypes = [
+ ctypes.c_char_p, # ip
+ c_int, # port
+ DataPacket, # packet
+ POINTER(c_bool) # out_success
+ ]
+ self.lib.mp_push_packet.restype = c_int
+
+ # mp_get_packet
+ self.lib.mp_get_packet.argtypes = [
+ ctypes.c_char_p, # ip
+ c_int, # port
+ hash_t, # packet_hash
+ POINTER(DataPacket) # out_packet
+ ]
+ self.lib.mp_get_packet.restype = c_int
+
+ # mp_get_firehose_packets
+ self.lib.mp_get_firehose_packets.argtypes = [
+ ctypes.c_char_p, # ip
+ c_int, # port
+ c_uint64, # offset
+ c_uint16, # count
+ POINTER(DataPacketList) # out_list
+ ]
+ self.lib.mp_get_firehose_packets.restype = c_int
+
+ # mp_create_packet_from_bytes
+ self.lib.mp_create_packet_from_bytes.argtypes = [
+ POINTER(byte), # input
+ c_size_t, # input_len
+ byte, # recipient_count
+ POINTER(hash_t), # recipients
+ KeyPair # author_keys
+ ]
+ self.lib.mp_create_packet_from_bytes.restype = DataPacket
+
+ # mp_create_packet_from_file
+ self.lib.mp_create_packet_from_file.argtypes = [
+ ctypes.c_char_p, # filename
+ byte, # recipient_count
+ POINTER(hash_t), # recipients
+ KeyPair, # author_keys
+ POINTER(DataPacket) # out_packet
+ ]
+ self.lib.mp_create_packet_from_file.restype = c_int
+
+ # mp_run_pow
+ self.lib.mp_run_pow.argtypes = [
+ DataPacket, # packet
+ byte # leading_zeros
+ ]
+ self.lib.mp_run_pow.restype = DataPacket
+
+ # mp_generate_keypair
+ self.lib.mp_generate_keypair.argtypes = []
+ self.lib.mp_generate_keypair.restype = KeyPair
+
+class MoonpasteClient:
+ '''High-level Python client for moonpaste'''
+
+ def __init__(self):
+ self.ffi = MoonpasteFFI()
+
+ def get_server_info(self, ip: str, port: int) -> PyRelayInfo:
+ '''Get server information and capabilities'''
+ info = RelayInfo()
+
+ result = self.ffi.lib.mp_get_server_info(
+ ip.encode('utf-8'),
+ port,
+ byref(info)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ return PyRelayInfo.from_c(info)
+
+ def get_neighbors(self, ip: str, port: int, count: int = 5) -> List[PyRelayNeighbor]:
+ '''Get a list of neighbors from the server'''
+ neighbors_ptr = POINTER(RelayNeighbor)()
+ out_count = byte()
+
+ result = self.ffi.lib.mp_get_server_neighbors(
+ ip.encode('utf-8'),
+ port,
+ count,
+ byref(neighbors_ptr),
+ byref(out_count)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ neighbors = []
+ if out_count.value > 0 and neighbors_ptr:
+ for i in range(out_count.value):
+ neighbors.append(PyRelayNeighbor.from_c(neighbors_ptr[i]))
+
+ # Free the allocated memory
+ ctypes.c_void_p.in_dll(self.ffi.lib, 'free').argtypes = [c_void_p]
+ ctypes.c_void_p.in_dll(self.ffi.lib, 'free')(cast(neighbors_ptr, c_void_p))
+
+ return neighbors
+
+ def push_packet(self, ip: str, port: int, packet: PyDataPacket) -> bool:
+ '''Push a packet to the server'''
+ c_packet = packet.to_c()
+ success = c_bool()
+
+ result = self.ffi.lib.mp_push_packet(
+ ip.encode('utf-8'),
+ port,
+ c_packet,
+ byref(success)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ return success.value
+
+ def get_packet(self, ip: str, port: int, packet_hash: bytes) -> PyDataPacket:
+ '''Get a specific packet by its hash'''
+ if len(packet_hash) != 32:
+ raise ValueError('Hash must be exactly 32 bytes')
+
+ c_hash = hash_t()
+ for i, b in enumerate(packet_hash):
+ c_hash[i] = b
+
+ packet = DataPacket()
+
+ result = self.ffi.lib.mp_get_packet(
+ ip.encode('utf-8'),
+ port,
+ c_hash,
+ byref(packet)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ return PyDataPacket.from_c(packet)
+
+ def get_firehose_packets(self, ip: str, port: int, offset: int = 0, count: int = 10) -> PyDataPacketList:
+ '''Get a batch of packets from the server's firehose stream'''
+ packet_list = DataPacketList()
+
+ result = self.ffi.lib.mp_get_firehose_packets(
+ ip.encode('utf-8'),
+ port,
+ offset,
+ count,
+ byref(packet_list)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ packets = []
+ if packet_list.count > 0 and packet_list.packets:
+ for i in range(packet_list.count):
+ packets.append(PyDataPacket.from_c(packet_list.packets[i]))
+
+ # Free the allocated memory safely
+ try:
+ import ctypes.util
+ libc_name = ctypes.util.find_library('c')
+ if libc_name:
+ libc = ctypes.CDLL(libc_name)
+ libc.free.argtypes = [c_void_p]
+ libc.free(cast(packet_list.packets, c_void_p))
+ else:
+ # Platform-specific fallback
+ try:
+ if sys.platform.startswith('linux'):
+ libc = ctypes.CDLL('libc.so.6')
+ elif sys.platform == 'darwin':
+ libc = ctypes.CDLL('libc.dylib')
+ elif sys.platform == 'win32':
+ libc = ctypes.CDLL('msvcrt.dll')
+ else:
+ libc = None
+
+ if libc:
+ libc.free.argtypes = [c_void_p]
+ libc.free(cast(packet_list.packets, c_void_p))
+ except OSError:
+ pass # Can't free, but better than crash
+ except:
+ pass # Memory will be freed when process exits
+
+ return PyDataPacketList(
+ packets=packets,
+ count=packet_list.count,
+ new_offset=packet_list.new_offset
+ )
+
+ def create_packet(self,
+ message: PyUnion[str, bytes],
+ recipients: Optional[List[bytes]] = None,
+ author_keys: Optional[PyKeyPair] = None) -> PyDataPacket:
+ '''Create a data packet from message content'''
+
+ if isinstance(message, str):
+ data = message.encode('utf-8')
+ else:
+ data = message
+
+ if author_keys is None:
+ author_keys = self.generate_keypair()
+
+ c_keys = author_keys.to_c()
+
+ # Convert data to ctypes array
+ data_len = min(len(data), DATA_SIZE)
+ c_data = (byte * data_len)(*data[:data_len])
+
+ # Handle recipients
+ recipient_count = 0
+ c_recipients = None
+
+ if recipients:
+ recipient_count = min(len(recipients), MAX_RECIPIENTS)
+ c_recipients = (hash_t * recipient_count)()
+ for i, recipient in enumerate(recipients[:recipient_count]):
+ if len(recipient) != 32:
+ raise ValueError(f'Recipient {i} must be exactly 32 bytes')
+ for j, b in enumerate(recipient):
+ c_recipients[i][j] = b
+
+ packet = self.ffi.lib.mp_create_packet_from_bytes(
+ cast(c_data, POINTER(byte)),
+ data_len,
+ recipient_count,
+ cast(c_recipients, POINTER(hash_t)) if c_recipients else None,
+ c_keys
+ )
+
+ return PyDataPacket.from_c(packet)
+
+ def create_packet_from_file(self,
+ filename: str,
+ recipients: Optional[List[bytes]] = None,
+ author_keys: Optional[PyKeyPair] = None) -> PyDataPacket:
+ '''Create a data packet from file contents'''
+
+ if author_keys is None:
+ author_keys = self.generate_keypair()
+
+ c_keys = author_keys.to_c()
+
+ # Handle recipients
+ recipient_count = 0
+ c_recipients = None
+
+ if recipients:
+ recipient_count = min(len(recipients), MAX_RECIPIENTS)
+ c_recipients = (hash_t * recipient_count)()
+ for i, recipient in enumerate(recipients[:recipient_count]):
+ if len(recipient) != 32:
+ raise ValueError(f'Recipient {i} must be exactly 32 bytes')
+ for j, b in enumerate(recipient):
+ c_recipients[i][j] = b
+
+ packet = DataPacket()
+
+ result = self.ffi.lib.mp_create_packet_from_file(
+ filename.encode('utf-8'),
+ recipient_count,
+ cast(c_recipients, POINTER(hash_t)) if c_recipients else None,
+ c_keys,
+ byref(packet)
+ )
+
+ if result != ClientError.SUCCESS:
+ raise MoonpasteClientError(ClientError(result))
+
+ return PyDataPacket.from_c(packet)
+
+ def run_proof_of_work(self, packet: PyDataPacket, leading_zeros: int) -> PyDataPacket:
+ '''Run Proof-of-Work on a packet'''
+ c_packet = packet.to_c()
+
+ result_packet = self.ffi.lib.mp_run_pow(c_packet, leading_zeros)
+
+ return PyDataPacket.from_c(result_packet)
+
+ def generate_keypair(self) -> PyKeyPair:
+ '''Generate a new encryption and signing keypair'''
+ c_keypair = self.ffi.lib.mp_generate_keypair()
+ return PyKeyPair.from_c(c_keypair)
+
+ # Utility methods
+ @staticmethod
+ def hash_to_hex(hash_bytes: bytes) -> str:
+ '''Convert a hash to hexadecimal string'''
+ return hash_bytes.hex()
+
+ @staticmethod
+ def hex_to_hash(hex_string: str) -> bytes:
+ '''Convert hexadecimal string to hash bytes'''
+ if len(hex_string) != 64:
+ raise ValueError('Hash hex string must be exactly 64 characters')
+ return bytes.fromhex(hex_string)
+
+ def create_and_push_packet(self,
+ ip: str,
+ port: int,
+ message: PyUnion[str, bytes],
+ leading_zeros: int = 0,
+ recipients: Optional[List[bytes]] = None,
+ author_keys: Optional[PyKeyPair] = None) -> Tuple[PyDataPacket, bool]:
+ '''Create a packet, run PoW, and push it to the server'''
+
+ # Create the packet
+ packet = self.create_packet(message, recipients, author_keys)
+
+ # Run proof of work if required
+ if leading_zeros > 0:
+ packet = self.run_proof_of_work(packet, leading_zeros)
+
+ # Push to server
+ success = self.push_packet(ip, port, packet)
+
+ return packet, success \ No newline at end of file
diff --git a/client/3rd-party/client.ts b/client/3rd-party/client.ts
new file mode 100644
index 0000000..44f4634
--- /dev/null
+++ b/client/3rd-party/client.ts
@@ -0,0 +1,674 @@
+// ai-generated (do not trust)
+
+import { dlopen, FFIType, JSCallback, suffix } from 'bun:ffi'
+import * as path from 'path'
+import * as fs from 'fs'
+
+export interface RelayNeighbor {
+ active: boolean
+ hasIpv4: boolean
+ hasIpv6: boolean
+ v4Addr: Uint8Array // 4 bytes
+ v6Addr: Uint8Array // 16 bytes
+}
+
+export interface RelayCapabilities {
+ retentionHours: number
+ canStore: boolean
+ leadingZeros: number
+ acceptsPlain: boolean
+}
+
+export interface RelayStatistics {
+ neighbors: number
+ heldPackets: bigint
+ quickConnect: RelayNeighbor[] // 4 elements
+}
+
+export interface RelayInfo {
+ majorVersion: number
+ minorVersion: number
+ motd: string // 128 bytes max
+ capabilities: RelayCapabilities
+ statistics: RelayStatistics
+}
+
+export interface DataPacketContents {
+ encrypted: boolean
+ createdTimeMs: bigint
+ author: Uint8Array // 32 bytes
+ authorSigningPub: Uint8Array // 32 bytes
+ recipientCount: number
+ recipients: Uint8Array[] // 32 arrays of 32 bytes each
+ data: Uint8Array // 65536 bytes
+ signature: Uint8Array // 64 bytes
+}
+
+export interface DataPacket {
+ nonce: number
+ hash: Uint8Array // 32 bytes
+ contents: DataPacketContents
+}
+
+export interface KeyPair {
+ encryptionPub: Uint8Array // 32 bytes
+ encryptionPriv: Uint8Array // 32 bytes
+ signingPub: Uint8Array // 32 bytes
+ signingPriv: Uint8Array // 64 bytes
+}
+
+export interface DataPacketList {
+ packets: DataPacket[]
+ count: number
+ newOffset: bigint
+}
+
+export enum ClientError {
+ Success = 0,
+ NetworkFail = 1,
+ InvalidAddress = 2,
+ ConnectionFailed = 3,
+ SendFailed = 4,
+ RecvFailed = 5,
+ ServerBusy = 6,
+ InvalidResponse = 7,
+ DeserializeFailed = 8,
+ AllocFailed = 9,
+ FileNotFound = 10,
+ FileReadFailed = 11,
+ BadArgument = 12
+}
+
+function getRuntime(): 'bun' | 'deno' | 'node' {
+ if (typeof Bun !== 'undefined') return 'bun'
+ if (typeof Deno !== 'undefined') return 'deno'
+ return 'node'
+}
+
+function findLibrary(): string {
+ const runtime = getRuntime()
+ const libNames = ['libmpclient.so', 'libmpclient.dylib', 'libmpclient.dll']
+ const searchPaths = [
+ './',
+ './lib/',
+ '../lib/',
+ '/usr/local/lib/',
+ '/usr/lib/',
+ process.env.MOONPASTE_LIB_PATH || ''
+ ].filter(p => p.length > 0)
+
+ for (const searchPath of searchPaths) {
+ for (const libName of libNames) {
+ const fullPath = path.join(searchPath, libName)
+ if (fs.existsSync(fullPath)) {
+ return fullPath
+ }
+ }
+ }
+
+ throw new Error('Could not find moonpaste client library. Searched for: ' + libNames.join(', '))
+}
+
+class MoonpasteFFI {
+ private lib: any
+ private runtime: string
+
+ constructor() {
+ this.runtime = getRuntime()
+ const libPath = findLibrary()
+
+ switch (this.runtime) {
+ case 'bun':
+ this.lib = dlopen(libPath, {
+ mp_get_server_info: {
+ args: [FFIType.cstring, FFIType.i32, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_get_server_neighbors: {
+ args: [FFIType.cstring, FFIType.i32, FFIType.u8, FFIType.ptr, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_push_packet: {
+ args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_get_packet: {
+ args: [FFIType.cstring, FFIType.i32, FFIType.ptr, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_get_firehose_packets: {
+ args: [FFIType.cstring, FFIType.i32, FFIType.u64, FFIType.u16, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_create_packet_from_bytes: {
+ args: [FFIType.ptr, FFIType.usize, FFIType.u8, FFIType.ptr, FFIType.ptr],
+ returns: FFIType.ptr
+ },
+ mp_create_packet_from_file: {
+ args: [FFIType.cstring, FFIType.u8, FFIType.ptr, FFIType.ptr, FFIType.ptr],
+ returns: FFIType.i32
+ },
+ mp_run_pow: {
+ args: [FFIType.ptr, FFIType.u8],
+ returns: FFIType.ptr
+ },
+ mp_generate_keypair: {
+ args: [],
+ returns: FFIType.ptr
+ }
+ })
+ break
+
+ case 'deno':
+ this.lib = Deno.dlopen(libPath, {
+ mp_get_server_info: {
+ parameters: ['buffer', 'i32', 'pointer'],
+ result: 'i32'
+ },
+ mp_get_server_neighbors: {
+ parameters: ['buffer', 'i32', 'u8', 'pointer', 'pointer'],
+ result: 'i32'
+ },
+ mp_push_packet: {
+ parameters: ['buffer', 'i32', 'pointer', 'pointer'],
+ result: 'i32'
+ },
+ mp_get_packet: {
+ parameters: ['buffer', 'i32', 'pointer', 'pointer'],
+ result: 'i32'
+ },
+ mp_get_firehose_packets: {
+ parameters: ['buffer', 'i32', 'u64', 'u16', 'pointer'],
+ result: 'i32'
+ },
+ mp_create_packet_from_bytes: {
+ parameters: ['pointer', 'usize', 'u8', 'pointer', 'pointer'],
+ result: 'pointer'
+ },
+ mp_create_packet_from_file: {
+ parameters: ['buffer', 'u8', 'pointer', 'pointer', 'pointer'],
+ result: 'i32'
+ },
+ mp_run_pow: {
+ parameters: ['pointer', 'u8'],
+ result: 'pointer'
+ },
+ mp_generate_keypair: {
+ parameters: [],
+ result: 'pointer'
+ }
+ })
+ break
+
+ case 'node':
+ try {
+ const ffi = require('ffi-napi') // dependency
+ const ref = require('ref-napi') // dependency
+
+ this.lib = ffi.Library(libPath, {
+ mp_get_server_info: ['int', ['string', 'int', 'pointer']],
+ mp_get_server_neighbors: ['int', ['string', 'int', 'uint8', 'pointer', 'pointer']],
+ mp_push_packet: ['int', ['string', 'int', 'pointer', 'pointer']],
+ mp_get_packet: ['int', ['string', 'int', 'pointer', 'pointer']],
+ mp_get_firehose_packets: ['int', ['string', 'int', 'uint64', 'uint16', 'pointer']],
+ mp_create_packet_from_bytes: ['pointer', ['pointer', 'size_t', 'uint8', 'pointer', 'pointer']],
+ mp_create_packet_from_file: ['int', ['string', 'uint8', 'pointer', 'pointer', 'pointer']],
+ mp_run_pow: ['pointer', ['pointer', 'uint8']],
+ mp_generate_keypair: ['pointer', []]
+ })
+ } catch (e) {
+ throw new Error('Node.js FFI requires ffi-napi and ref-napi packages: npm install ffi-napi ref-napi')
+ }
+ break
+ }
+ }
+
+ private allocateBuffer(size: number): any {
+ switch (this.runtime) {
+ case 'bun':
+ return new ArrayBuffer(size)
+ case 'deno':
+ return new ArrayBuffer(size)
+ case 'node':
+ const ref = require('ref-napi')
+ return ref.alloc(size)
+ }
+ }
+
+ private stringToBuffer(str: string): any {
+ switch (this.runtime) {
+ case 'bun':
+ case 'deno':
+ return new TextEncoder().encode(str + '\0')
+ case 'node':
+ return Buffer.from(str + '\0', 'utf8')
+ }
+ }
+
+ async getServerInfo(ip: string, port: number): Promise<RelayInfo> {
+ const infoBuffer = this.allocateBuffer(1024)
+ const ipBuffer = this.stringToBuffer(ip)
+
+ const result = this.lib.symbols.mp_get_server_info(ipBuffer, port, infoBuffer)
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to get server info: ${result}`)
+ }
+
+ return this.parseRelayInfo(infoBuffer)
+ }
+
+ async getServerNeighbors(ip: string, port: number, count: number): Promise<RelayNeighbor[]> {
+ const neighborsPtr = this.allocateBuffer(8) // pointer
+ const countPtr = this.allocateBuffer(1) // uint8
+ const ipBuffer = this.stringToBuffer(ip)
+
+ const result = this.lib.symbols.mp_get_server_neighbors(ipBuffer, port, count, neighborsPtr, countPtr)
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to get neighbors: ${result}`)
+ }
+
+ return this.parseNeighbors(neighborsPtr, countPtr)
+ }
+
+ async pushPacket(ip: string, port: number, packet: DataPacket): Promise<boolean> {
+ const successPtr = this.allocateBuffer(1)
+ const ipBuffer = this.stringToBuffer(ip)
+ const packetBuffer = this.serializePacket(packet)
+
+ const result = this.lib.symbols.mp_push_packet(ipBuffer, port, packetBuffer, successPtr)
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to push packet: ${result}`)
+ }
+
+ return this.readBool(successPtr)
+ }
+
+ async getPacket(ip: string, port: number, hash: Uint8Array): Promise<DataPacket> {
+ const packetBuffer = this.allocateBuffer(70000) // approximation
+ const ipBuffer = this.stringToBuffer(ip)
+
+ const result = this.lib.symbols.mp_get_packet(ipBuffer, port, hash, packetBuffer)
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to get packet: ${result}`)
+ }
+
+ return this.parseDataPacket(packetBuffer)
+ }
+
+ async getFirehosePackets(ip: string, port: number, offset: bigint, count: number): Promise<DataPacketList> {
+ const listBuffer = this.allocateBuffer(1024)
+ const ipBuffer = this.stringToBuffer(ip)
+
+ const result = this.lib.symbols.mp_get_firehose_packets(ipBuffer, port, offset, count, listBuffer)
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to get firehose packets: ${result}`)
+ }
+
+ return this.parseDataPacketList(listBuffer)
+ }
+
+ createPacketFromBytes(input: Uint8Array, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): DataPacket {
+ const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null
+ const keysBuffer = this.serializeKeyPair(authorKeys)
+
+ const result = this.lib.symbols.mp_create_packet_from_bytes(
+ input,
+ input.length,
+ recipientCount,
+ recipientsBuffer,
+ keysBuffer
+ )
+
+ return this.parseDataPacket(result)
+ }
+
+ async createPacketFromFile(filename: string, recipientCount: number, recipients: Uint8Array[] | null, authorKeys: KeyPair): Promise<DataPacket> {
+ const packetBuffer = this.allocateBuffer(4096)
+ const filenameBuffer = this.stringToBuffer(filename)
+ const recipientsBuffer = recipients ? this.serializeRecipients(recipients) : null
+ const keysBuffer = this.serializeKeyPair(authorKeys)
+
+ const result = this.lib.symbols.mp_create_packet_from_file(
+ filenameBuffer,
+ recipientCount,
+ recipientsBuffer,
+ keysBuffer,
+ packetBuffer
+ )
+
+ if (result !== ClientError.Success) {
+ throw new Error(`Failed to create packet from file: ${result}`)
+ }
+
+ return this.parseDataPacket(packetBuffer)
+ }
+
+ runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket {
+ const packetBuffer = this.serializePacket(packet)
+ const result = this.lib.symbols.mp_run_pow(packetBuffer, leadingZeros)
+ return this.parseDataPacket(result)
+ }
+
+ generateKeyPair(): KeyPair {
+ const result = this.lib.symbols.mp_generate_keypair()
+ return this.parseKeyPair(result)
+ }
+
+ private parseRelayInfo(buffer: any): RelayInfo {
+ const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer)
+ let offset = 0
+
+ const majorVersion = view.getUint8(offset++)
+ const minorVersion = view.getUint8(offset++)
+
+ const motdBytes = new Uint8Array(buffer, offset, 128)
+ const motd = new TextDecoder().decode(motdBytes).replace(/\0.*$/, '')
+ offset += 128
+
+ const retentionHours = view.getUint16(offset, true) // little endian
+ offset += 2
+ const canStore = view.getUint8(offset++) !== 0
+ const leadingZeros = view.getUint8(offset++)
+ const acceptsPlain = view.getUint8(offset++) !== 0
+
+ const neighbors = view.getUint16(offset, true)
+ offset += 2
+ const heldPackets = view.getBigUint64(offset, true)
+ offset += 8
+
+ const quickConnect: RelayNeighbor[] = []
+ for (let i = 0; i < 4; i++) {
+ quickConnect.push(this.parseRelayNeighborAt(buffer, offset))
+ offset += 23 // 1 + 1 + 1 + 4 + 16
+ }
+
+ return {
+ majorVersion,
+ minorVersion,
+ motd,
+ capabilities: {
+ retentionHours,
+ canStore,
+ leadingZeros,
+ acceptsPlain
+ },
+ statistics: {
+ neighbors,
+ heldPackets,
+ quickConnect
+ }
+ }
+ }
+
+ private parseRelayNeighborAt(buffer: any, offset: number): RelayNeighbor {
+ const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer)
+
+ const active = view.getUint8(offset) !== 0
+ const hasIpv4 = view.getUint8(offset + 1) !== 0
+ const hasIpv6 = view.getUint8(offset + 2) !== 0
+
+ const v4Addr = new Uint8Array(buffer, offset + 3, 4)
+ const v6Addr = new Uint8Array(buffer, offset + 7, 16)
+
+ return { active, hasIpv4, hasIpv6, v4Addr, v6Addr }
+ }
+
+ private parseNeighbors(neighborsPtr: any, countPtr: any): RelayNeighbor[] {
+ const count = this.readUint8(countPtr)
+ const neighbors: RelayNeighbor[] = []
+
+ if (count > 0 && neighborsPtr) {
+ for (let i = 0; i < count; i++) {
+ const neighborOffset = i * 23 // RELAY_NEIGHBOR_WIRE_LEN
+ neighbors.push(this.parseRelayNeighborAt(neighborsPtr, neighborOffset))
+ }
+ }
+
+ return neighbors
+ }
+
+ private parseDataPacket(buffer: any): DataPacket {
+ const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer)
+ let offset = 0
+
+ const nonce = view.getUint32(offset, true)
+ offset += 4
+
+ const hash = new Uint8Array(buffer, offset, 32)
+ offset += 32
+
+ const encrypted = view.getUint8(offset++) !== 0
+ const createdTimeMs = view.getBigUint64(offset, true)
+ offset += 8
+
+ const author = new Uint8Array(buffer, offset, 32)
+ offset += 32
+ const authorSigningPub = new Uint8Array(buffer, offset, 32)
+ offset += 32
+
+ const recipientCount = view.getUint8(offset++)
+ const recipients: Uint8Array[] = []
+ for (let i = 0; i < 32; i++) {
+ recipients.push(new Uint8Array(buffer, offset, 32))
+ offset += 32
+ }
+
+ const data = new Uint8Array(buffer, offset, 65536)
+ offset += 65536
+
+ const signature = new Uint8Array(buffer, offset, 64)
+
+ return {
+ nonce,
+ hash,
+ contents: {
+ encrypted,
+ createdTimeMs,
+ author,
+ authorSigningPub,
+ recipientCount,
+ recipients,
+ data,
+ signature
+ }
+ }
+ }
+
+ private parseDataPacketList(buffer: any): DataPacketList {
+ const view = new DataView(buffer instanceof ArrayBuffer ? buffer : buffer.buffer)
+
+ const newOffset = view.getBigUint64(0, true)
+ const availableBytes = buffer.byteLength - 8
+ const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64) // PACKET_SIZE
+ const packetCount = Math.floor(availableBytes / packetSize)
+
+ const packets: DataPacket[] = []
+ for (let i = 0; i < packetCount; i++) {
+ const packetOffset = 8 + (i * packetSize)
+ const packetBuffer = buffer.slice(packetOffset, packetOffset + packetSize)
+ packets.push(this.parseDataPacket(packetBuffer))
+ }
+
+ return {
+ packets,
+ count: packetCount,
+ newOffset
+ }
+ }
+
+ private parseKeyPair(buffer: any): KeyPair {
+ const encryptionPub = new Uint8Array(buffer, 0, 32)
+ const encryptionPriv = new Uint8Array(buffer, 32, 32)
+ const signingPub = new Uint8Array(buffer, 64, 32)
+ const signingPriv = new Uint8Array(buffer, 96, 64)
+
+ return {
+ encryptionPub,
+ encryptionPriv,
+ signingPub,
+ signingPriv
+ }
+ }
+
+ private serializePacket(packet: DataPacket): any {
+ const packetSize = 4 + 32 + (1 + 8 + 32 + 32 + 1 + (32 * 32) + 65536 + 64)
+ const buffer = new ArrayBuffer(packetSize)
+ const view = new DataView(buffer)
+ let offset = 0
+
+ view.setUint32(offset, packet.nonce, true)
+ offset += 4
+
+ new Uint8Array(buffer, offset, 32).set(packet.hash)
+ offset += 32
+
+ view.setUint8(offset++, packet.contents.encrypted ? 1 : 0)
+ view.setBigUint64(offset, packet.contents.createdTimeMs, true)
+ offset += 8
+
+ new Uint8Array(buffer, offset, 32).set(packet.contents.author)
+ offset += 32
+ new Uint8Array(buffer, offset, 32).set(packet.contents.authorSigningPub)
+ offset += 32
+
+ view.setUint8(offset++, packet.contents.recipientCount)
+ for (let i = 0; i < 32; i++) {
+ if (i < packet.contents.recipients.length) {
+ new Uint8Array(buffer, offset, 32).set(packet.contents.recipients[i])
+ }
+ offset += 32
+ }
+
+ new Uint8Array(buffer, offset, 65536).set(packet.contents.data)
+ offset += 65536
+
+ new Uint8Array(buffer, offset, 64).set(packet.contents.signature)
+
+ return buffer
+ }
+
+ private serializeRecipients(recipients: Uint8Array[]): any {
+ const buffer = new ArrayBuffer(32 * 32) // MAX_RECIPIENTS * 32 bytes each
+ for (let i = 0; i < Math.min(recipients.length, 32); i++) {
+ new Uint8Array(buffer, i * 32, 32).set(recipients[i])
+ }
+ return buffer
+ }
+
+ private serializeKeyPair(keyPair: KeyPair): any {
+ const buffer = new ArrayBuffer(32 + 32 + 32 + 64) // total keypair size
+ let offset = 0
+
+ new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPub)
+ offset += 32
+ new Uint8Array(buffer, offset, 32).set(keyPair.encryptionPriv)
+ offset += 32
+ new Uint8Array(buffer, offset, 32).set(keyPair.signingPub)
+ offset += 32
+ new Uint8Array(buffer, offset, 64).set(keyPair.signingPriv)
+
+ return buffer
+ }
+
+ private readBool(ptr: any): boolean {
+ return this.readUint8(ptr) !== 0
+ }
+
+ private readUint8(ptr: any): number {
+ switch (this.runtime) {
+ case 'bun':
+ case 'deno':
+ return new Uint8Array(ptr)[0]
+ case 'node':
+ return ptr.readUInt8(0)
+ default:
+ return 0
+ }
+ }
+}
+
+// Main client class
+export class MoonpasteClient {
+ private ffi: MoonpasteFFI
+
+ constructor() {
+ this.ffi = new MoonpasteFFI()
+ }
+
+ async getServerInfo(ip: string, port: number): Promise<RelayInfo> {
+ return this.ffi.getServerInfo(ip, port)
+ }
+
+ async getNeighbors(ip: string, port: number, count: number = 5): Promise<RelayNeighbor[]> {
+ return this.ffi.getServerNeighbors(ip, port, count)
+ }
+
+ async pushPacket(ip: string, port: number, packet: DataPacket): Promise<boolean> {
+ return this.ffi.pushPacket(ip, port, packet)
+ }
+
+ async getPacket(ip: string, port: number, hash: Uint8Array): Promise<DataPacket> {
+ return this.ffi.getPacket(ip, port, hash)
+ }
+
+ async getFirehosePackets(ip: string, port: number, offset: bigint = 0n, count: number = 10): Promise<DataPacketList> {
+ return this.ffi.getFirehosePackets(ip, port, offset, count)
+ }
+
+ createPacket(message: string | Uint8Array, recipients?: Uint8Array[], authorKeys?: KeyPair): DataPacket {
+ const data = typeof message === 'string' ? new TextEncoder().encode(message) : message
+ const keys = authorKeys || this.generateKeyPair()
+ const recipientCount = recipients ? recipients.length : 0
+
+ return this.ffi.createPacketFromBytes(data, recipientCount, recipients || null, keys)
+ }
+
+ async createPacketFromFile(filename: string, recipients?: Uint8Array[], authorKeys?: KeyPair): Promise<DataPacket> {
+ const keys = authorKeys || this.generateKeyPair()
+ const recipientCount = recipients ? recipients.length : 0
+
+ return this.ffi.createPacketFromFile(filename, recipientCount, recipients || null, keys)
+ }
+
+ runProofOfWork(packet: DataPacket, leadingZeros: number): DataPacket {
+ return this.ffi.runProofOfWork(packet, leadingZeros)
+ }
+
+ generateKeyPair(): KeyPair {
+ return this.ffi.generateKeyPair()
+ }
+
+ // Utility methods
+ static hashToHex(hash: Uint8Array): string {
+ return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('')
+ }
+
+ static hexToHash(hex: string): Uint8Array {
+ const result = new Uint8Array(32)
+ for (let i = 0; i < 32; i++) {
+ result[i] = parseInt(hex.substr(i * 2, 2), 16)
+ }
+ return result
+ }
+
+ static formatIpAddress(neighbor: RelayNeighbor): string {
+ if (neighbor.hasIpv4) {
+ return Array.from(neighbor.v4Addr).join('.')
+ } else if (neighbor.hasIpv6) {
+ const parts: string[] = []
+ for (let i = 0; i < 8; i++) {
+ const word = (neighbor.v6Addr[i * 2] << 8) | neighbor.v6Addr[i * 2 + 1]
+ parts.push(word.toString(16))
+ }
+ return parts.join(':')
+ }
+ return 'N/A'
+ }
+}
+
+export default MoonpasteClient \ No newline at end of file