PHP Classes

File: scripts/lovlynet_setup.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   scripts/lovlynet_setup.php   Download  
File: scripts/lovlynet_setup.php
Role: Example script
Content type: text/plain
Description: Example script
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 26,562 bytes
 

Contents

Class file image Download
#!/usr/bin/env php <?php /** * LovlyNet Automatic Network Registration Tool * * Registers this BBS with the LovlyNet registry, receives an FTN address, * configures the uplink, creates echo areas, and sends an areafix request. * * Usage: * php lovlynet_setup.php # Interactive registration * php lovlynet_setup.php --status # Show current registration status * php lovlynet_setup.php --update # Re-register / update existing registration */ require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/../src/functions.php'; use BinktermPHP\Binkp\Config\BinkpConfig; use BinktermPHP\Config; use BinktermPHP\Database; use BinktermPHP\EchoareaManager; use BinktermPHP\FileAreaManager; use BinktermPHP\LovlyNetClient; use BinktermPHP\MessageHandler; use BinktermPHP\NetworkManager; use BinktermPHP\Version; define('LOVLYNET_REGISTRY_URL', 'https://lovlynet.lovelybits.org/api/register.php'); define('LOVLYNET_CONFIG_PATH', __DIR__ . '/../config/lovlynet.json'); define('LOVLYNET_DOMAIN', 'lovlynet'); /** * Read a line of input from the user with an optional default value. * * @param string $prompt The prompt text * @param string $default Default value shown in brackets * @return string The user's input or the default */ function readInput($prompt, $default = '') { if ($default !== '') { echo "{$prompt} [{$default}]: "; } else { echo "{$prompt}: "; } $input = trim(fgets(STDIN)); return ($input !== '') ? $input : $default; } /** * Read a yes/no confirmation from the user. * * @param string $prompt The question to ask * @param bool $default Default answer (true = yes) * @return bool */ function confirm($prompt, $default = true) { $hint = $default ? 'Y/n' : 'y/N'; echo "{$prompt} [{$hint}]: "; $input = strtolower(trim(fgets(STDIN))); if ($input === '') { return $default; } return in_array($input, ['y', 'yes']); } /** * Load existing LovlyNet registration config. * * @return array|null The config array or null if not registered */ function loadLovlyNetConfig() { if (!file_exists(LOVLYNET_CONFIG_PATH)) { return null; } $json = file_get_contents(LOVLYNET_CONFIG_PATH); $config = json_decode($json, true); if (json_last_error() !== JSON_ERROR_NONE) { return null; } return $config; } /** * Save LovlyNet registration config. * * @param array $config The config to save * @return bool */ function saveLovlyNetConfig($config) { $configDir = dirname(LOVLYNET_CONFIG_PATH); if (!is_dir($configDir)) { mkdir($configDir, 0755, true); } return file_put_contents( LOVLYNET_CONFIG_PATH, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ) !== false; } /** * Derive the LovlyNet TIC password from the Areafix password. * * @param string $areafixPassword * @return string */ function deriveLovlyNetTicPassword(string $areafixPassword): string { return strtoupper(substr($areafixPassword, 0, 8)); } /** * Ensure the LovlyNet network-level settings exist in the networks table. */ function ensureLovlyNetNetworkDefaults(): void { try { $manager = new NetworkManager(); $manager->upsertLovlyNetDefaults(); } catch (\Throwable $e) { echo "Warning: Could not update LovlyNet network defaults: " . $e->getMessage() . "\n"; } } /** * Pull currently subscribed LovlyNet areas from the API and ensure matching * local echo/file areas exist. * * @param string $hubAddress * @return void */ function syncSubscribedLovlyNetAreas(string $hubAddress): void { echo "Syncing subscribed LovlyNet areas... "; try { $client = new LovlyNetClient(); $areasResult = $client->getAreas(); if (!$areasResult['success']) { throw new \RuntimeException($areasResult['error'] ?? 'Failed to load areas'); } $echoareaManager = new EchoareaManager(); $fileAreaManager = new FileAreaManager(); $echoCreated = 0; $echoSkipped = 0; $fileCreated = 0; $fileSkipped = 0; $client = new LovlyNetClient(); foreach (($areasResult['echoareas'] ?? []) as $area) { if (empty($area['subscribed'])) { continue; } $tag = trim((string)($area['tag'] ?? '')); if ($tag === '') { continue; } $metadata = isset($area['metadata']) && is_array($area['metadata']) ? $area['metadata'] : []; $isSysopOnly = false; if (isset($metadata['sysop_only'])) { $v = filter_var($metadata['sysop_only'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($v !== null) { $isSysopOnly = $v; } } $existingId = $echoareaManager->findByTagAndDomains($tag, ['', LOVLYNET_DOMAIN])['id'] ?? null; $localId = $echoareaManager->createIfMissing([ 'tag' => $tag, 'description' => trim((string)($area['description'] ?? '')), 'domain' => LOVLYNET_DOMAIN, 'uplink_address' => $hubAddress, 'is_local' => false, 'is_active' => true, 'is_sysop_only' => $isSysopOnly, 'gemini_public' => false, ], ['', LOVLYNET_DOMAIN]); if (!$existingId) { // Default new LovlyNet areas to media-enabled; applyRecommendedSettings // will override this if LovlyNet's metadata specifies a different value. $echoareaManager->updateAllowMedia($localId, true); } $client->applyRecommendedSettings('echo', array_merge($area, ['local_echoarea_id' => $localId])); if ($existingId) { $echoSkipped++; } else { $echoCreated++; } } foreach (($areasResult['fileareas'] ?? []) as $area) { if (empty($area['subscribed'])) { continue; } $tag = trim((string)($area['tag'] ?? '')); if ($tag === '') { continue; } $metadata = isset($area['metadata']) && is_array($area['metadata']) ? $area['metadata'] : []; $uploadPermission = FileAreaManager::getDefaultUploadPermissionForArea($tag, LOVLYNET_DOMAIN); if (isset($metadata['readonly'])) { $v = filter_var($metadata['readonly'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($v !== null) { $uploadPermission = $v ? FileAreaManager::UPLOAD_READ_ONLY : FileAreaManager::UPLOAD_USERS_ALLOWED; } } $replaceExisting = true; if (isset($metadata['replace'])) { $v = filter_var($metadata['replace'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($v !== null) { $replaceExisting = $v; } } $existingId = $fileAreaManager->getFileAreaByTag(strtoupper($tag), LOVLYNET_DOMAIN)['id'] ?? null; $localId = $fileAreaManager->createIfMissing([ 'tag' => $tag, 'description' => trim((string)($area['description'] ?? '')), 'domain' => LOVLYNET_DOMAIN, 'is_local' => false, 'is_active' => true, 'upload_permission' => $uploadPermission, 'replace_existing' => $replaceExisting, ]); $client->applyRecommendedSettings('file', array_merge($area, ['local_filearea_id' => $localId])); if ($existingId) { $fileSkipped++; } else { $fileCreated++; } } echo "OK ({$echoCreated} echo created, {$echoSkipped} echo existing, {$fileCreated} file created, {$fileSkipped} file existing)\n"; } catch (\Throwable $e) { echo "FAILED\n"; echo "Note: " . $e->getMessage() . "\n"; echo "You can sync the areas later from the LovlyNet admin page.\n"; } } /** * Show current registration status. */ function showStatus() { $config = loadLovlyNetConfig(); if (!$config) { echo "Not registered with LovlyNet.\n"; echo "Run 'php lovlynet_setup.php' to register.\n"; return; } echo "\nLovlyNet Registration Status\n"; echo str_repeat("=", 40) . "\n"; echo "FTN Address: {$config['ftn_address']}\n"; echo "Hub Address: {$config['hub_address']}\n"; echo "Registered: {$config['registered_at']}\n"; if (!empty($config['updated_at'])) { echo "Last Updated: {$config['updated_at']}\n"; } echo "\nAPI Key: " . substr($config['api_key'], 0, 8) . "..." . substr($config['api_key'], -4) . "\n"; echo "\n"; } /** * Register or update this BBS with the LovlyNet registry. * * @param bool $isUpdate Whether this is an update of an existing registration */ function doRegistration($isUpdate = false) { $binkpConfig = BinkpConfig::getInstance(); $existingConfig = loadLovlyNetConfig(); echo "\n"; echo "============================================\n"; echo " LovlyNet Network Registration\n"; echo " Zone 227 - Powered by BinktermPHP\n"; echo "============================================\n\n"; // Check for existing LovlyNet uplink (manual configuration) $requestedFtnNode = null; if (!$existingConfig) { $fullConfig = $binkpConfig->getFullConfig(); if (isset($fullConfig['uplinks'])) { foreach ($fullConfig['uplinks'] as $uplink) { if (isset($uplink['domain']) && $uplink['domain'] === LOVLYNET_DOMAIN) { // Found existing LovlyNet uplink - extract node number from 'me' field if (isset($uplink['me']) && preg_match('/227:1\/(\d+)/', $uplink['me'], $matches)) { $requestedFtnNode = (int)$matches[1]; echo "Found existing LovlyNet uplink.\n"; echo "Your address: {$uplink['me']}\n"; echo "Will request node number {$requestedFtnNode} from registry.\n\n"; } break; } } } } if ($existingConfig && !$isUpdate) { echo "You are already registered with LovlyNet.\n"; echo "FTN Address: {$existingConfig['ftn_address']}\n\n"; if (!confirm("Would you like to update your registration?")) { echo "Registration cancelled.\n"; return; } $isUpdate = true; } if ($isUpdate && !$existingConfig) { echo "No existing registration found. Proceeding with new registration.\n\n"; $isUpdate = false; } // Gather information $systemName = $binkpConfig->getSystemName(); $sysopName = $binkpConfig->getSystemSysop(); $hostname = $binkpConfig->getSystemHostname(); $binkpPort = $binkpConfig->getBinkpPort(); echo "Please verify the following information:\n\n"; $systemName = readInput("System Name", $systemName); $sysopName = readInput("Sysop Name", $sysopName); // Ask if the BBS is publicly accessible echo "\n"; $isPublic = confirm("Is your BBS accessible from the public internet?", true); echo "\n"; if ($isPublic) { // Public node - will accept inbound connections echo "Public Node Configuration\n"; echo str_repeat("-", 40) . "\n"; echo "Your node will be able to receive inbound binkp connections.\n\n"; $hostname = readInput("Public Hostname/IP (for binkp connections)", $hostname); $binkpPort = (int)readInput("Binkp Port", (string)$binkpPort); // Default site URL to https://<hostname> $defaultSiteUrl = 'https://' . $hostname; // Override with SITE_URL env var if set $envSiteUrl = getenv('SITE_URL'); if ($envSiteUrl) { $defaultSiteUrl = rtrim($envSiteUrl, '/'); } $siteUrl = readInput("Site URL (public web address)", $defaultSiteUrl); if (empty($siteUrl)) { echo "\nWarning: No site URL provided. Verification will not be possible.\n"; $siteUrl = 'http://localhost'; } // Verify the /api/verify endpoint is accessible locally echo "\nVerifying local /api/verify endpoint... "; $verifyUrl = rtrim($siteUrl, '/') . '/api/verify'; $context = stream_context_create([ 'http' => ['method' => 'GET', 'timeout' => 10, 'ignore_errors' => true], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] ]); $response = @file_get_contents($verifyUrl, false, $context); if ($response === false) { echo "FAILED\n"; echo "Note: The registry will attempt verification, but it may fail.\n"; } else { $verifyData = json_decode($response, true); if (isset($verifyData['system_name'])) { echo "OK ({$verifyData['system_name']})\n"; } else { echo "WARNING: Unexpected response format\n"; } } } else { // Passive node - poll-only (community wireless, NAT, etc.) echo "Passive Node Configuration\n"; echo str_repeat("-", 40) . "\n"; echo "Your node will poll the hub for mail but will NOT accept\n"; echo "inbound connections. This is suitable for:\n"; echo " - Community wireless networks\n"; echo " - Nodes behind NAT/firewall\n"; echo " - Dynamic IP addresses\n"; echo " - Development/testing systems\n\n"; $hostname = 'passive.lovelybits.org'; $binkpPort = 24554; $siteUrl = 'http://localhost'; echo "Using passive node defaults:\n"; echo " Hostname: {$hostname} (not used for inbound)\n"; echo " Your node will poll the hub for mail\n"; } echo "\n"; echo "Registration Summary:\n"; echo str_repeat("=", 50) . "\n"; echo " System Name: {$systemName}\n"; echo " Sysop: {$sysopName}\n"; echo " Node Type: " . ($isPublic ? "Public (accepts inbound)" : "Passive (poll-only)") . "\n"; echo " Hostname: {$hostname}\n"; echo " Binkp Port: {$binkpPort}\n"; echo " Site URL: {$siteUrl}\n"; echo " Software: " . Version::getFullVersion() . "\n"; echo str_repeat("=", 50) . "\n"; echo "\n"; if (!confirm("Proceed with registration?")) { echo "Registration cancelled.\n"; return; } // Build request payload $payload = [ 'system_name' => $systemName, 'hostname' => $hostname, 'site_url' => $siteUrl, 'sysop_name' => $sysopName, 'binkp_port' => $binkpPort, 'is_passive' => !$isPublic, 'system_info' => [ 'software' => Version::getFullVersion(), 'php_version' => PHP_VERSION ] ]; // Include node_id for updates if ($isUpdate && $existingConfig && !empty($existingConfig['node_id'])) { $payload['node_id'] = $existingConfig['node_id']; } // Include requested FTN node number if found from existing uplink if ($requestedFtnNode !== null) { $payload['requested_ftn_node'] = $requestedFtnNode; } // Build HTTP headers $headers = ['Content-Type: application/json']; if ($isUpdate && $existingConfig) { $headers[] = 'X-API-Key: ' . $existingConfig['api_key']; } echo "\nRegistering with LovlyNet... "; $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => implode("\r\n", $headers), 'content' => json_encode($payload), 'timeout' => 30, 'ignore_errors' => true ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true ] ]); $response = @file_get_contents(LOVLYNET_REGISTRY_URL, false, $context); if ($response === false) { echo "FAILED\n\n"; echo "Could not connect to the LovlyNet registry at:\n"; echo " " . LOVLYNET_REGISTRY_URL . "\n\n"; echo "Check your internet connection and try again.\n"; return; } $result = json_decode($response, true); if (!$result || !$result['success']) { echo "FAILED\n\n"; echo "Registry error: " . ($result['message'] ?? 'Unknown error') . "\n"; return; } echo "OK\n\n"; $regData = $result['data']; echo "Registration successful!\n"; echo str_repeat("=", 40) . "\n"; echo "FTN Address: {$regData['ftn_address']}\n"; echo "Hub Address: {$regData['hub_address']}\n"; echo "Hub Hostname: {$regData['hub_hostname']}\n"; echo "Hub Port: {$regData['hub_port']}\n"; echo "Binkp Password: {$regData['binkp_password']}\n"; echo "Areafix Password: {$regData['areafix_password']}\n"; if (!empty($regData['api_key'])) { echo "API Key: " . substr($regData['api_key'], 0, 8) . "...\n"; } echo "\n"; // Save registration config $ticPassword = deriveLovlyNetTicPassword((string)$regData['areafix_password']); $lovlyNetConfig = [ 'node_id' => $regData['node_id'] ?? ($existingConfig['node_id'] ?? null), 'api_key' => $regData['api_key'] ?? ($existingConfig['api_key'] ?? ''), 'ftn_address' => $regData['ftn_address'], 'hub_address' => $regData['hub_address'], 'hub_hostname' => $regData['hub_hostname'], 'hub_port' => $regData['hub_port'], 'binkp_password' => $regData['binkp_password'], 'areafix_password' => $regData['areafix_password'], 'tic_password' => $ticPassword, 'is_passive' => !$isPublic, 'registered_at' => $existingConfig['registered_at'] ?? date('c'), 'updated_at' => date('c') ]; echo "Saving registration to config/lovlynet.json... "; if (saveLovlyNetConfig($lovlyNetConfig)) { echo "OK\n"; } else { echo "FAILED\n"; echo "Warning: Could not save registration config. You may need to re-register.\n"; } // Configure binkp uplink ensureLovlyNetNetworkDefaults(); echo "Configuring LovlyNet uplink... "; try { $existingUplink = $binkpConfig->getUplinkByDomain(LOVLYNET_DOMAIN); if ($existingUplink) { // Update existing uplink $binkpConfig->updateUplink($existingUplink['address'], [ 'me' => $regData['ftn_address'], 'address' => $regData['hub_address'], 'hostname' => $regData['hub_hostname'], 'port' => $regData['hub_port'], 'password' => $regData['binkp_password'], 'tic_password' => $ticPassword, 'domain' => LOVLYNET_DOMAIN, 'networks' => ['227:*/*'], 'enabled' => true, 'compression' => false, 'crypt' => true, 'poll_schedule' => '*/15 * * * *', ]); } else { // Add new uplink $binkpConfig->addUplink( $regData['hub_address'], $regData['hub_hostname'], $regData['hub_port'], $regData['binkp_password'], [ 'me' => $regData['ftn_address'], 'domain' => LOVLYNET_DOMAIN, 'networks' => ['227:*/*'], 'tic_password' => $ticPassword, 'compression' => false, 'crypt' => true, 'poll_schedule' => '*/15 * * * *', ] ); } echo "OK\n"; } catch (\Exception $e) { echo "FAILED\n"; echo "Error: " . $e->getMessage() . "\n"; echo "You may need to manually configure the uplink in config/binkp.json\n"; } syncSubscribedLovlyNetAreas($regData['hub_address']); // Send areafix netmail to hub echo "Sending areafix request to hub... "; try { $db = Database::getInstance()->getPdo(); // Find the sysop user (admin) to send from $stmt = $db->prepare(" SELECT id FROM users WHERE LOWER(real_name) = LOWER(?) OR LOWER(username) = LOWER(?) LIMIT 1 "); $stmt->execute([$sysopName, $sysopName]); $sysopUser = $stmt->fetch(); if (!$sysopUser) { // Fall back to first admin user $stmt = $db->prepare("SELECT id FROM users WHERE is_admin = TRUE ORDER BY id LIMIT 1"); $stmt->execute(); $sysopUser = $stmt->fetch(); } if (!$sysopUser) { echo "SKIPPED (no sysop user found)\n"; echo "Create an admin user first, then send areafix manually.\n"; } else { $messageHandler = new MessageHandler(); $areafixSubject = $regData['areafix_password']; $messageHandler->sendNetmail( $sysopUser['id'], $regData['hub_address'], 'AreaFix', $areafixSubject, "%HELP\n", $sysopName ); echo "OK\n"; } } catch (\Exception $e) { echo "SKIPPED\n"; echo "Note: " . $e->getMessage() . "\n"; echo "You can send an areafix netmail manually later.\n"; } // Send welcome message from hub echo "Sending welcome message... "; try { if (!isset($sysopUser) || !$sysopUser) { // Try to find sysop user again if not already set $db = Database::getInstance()->getPdo(); $stmt = $db->prepare(" SELECT id, real_name FROM users WHERE LOWER(real_name) = LOWER(?) OR LOWER(username) = LOWER(?) LIMIT 1 "); $stmt->execute([$sysopName, $sysopName]); $sysopUser = $stmt->fetch(); if (!$sysopUser) { $stmt = $db->prepare("SELECT id, real_name FROM users WHERE is_admin = TRUE ORDER BY id LIMIT 1"); $stmt->execute(); $sysopUser = $stmt->fetch(); } } if (!$sysopUser) { echo "SKIPPED (no user found)\n"; } else { // Read welcome template $templatePath = __DIR__ . '/../config/lovlynet_welcome.txt'; if (!file_exists($templatePath)) { echo "SKIPPED (template not found)\n"; } else { $welcomeText = file_get_contents($templatePath); // Replace placeholders $nodeType = $isPublic ? "Public (accepts inbound connections)" : "Passive (poll-only)"; $pollingInfo = $isPublic ? "Your node accepts inbound connections. The hub will deliver mail directly.\nOptional: Set up polling as a fallback (every 30 minutes recommended)." : "Your node is passive (poll-only). You MUST poll the hub regularly.\nRecommended: Set up a cron job to poll every 15-30 minutes."; $welcomeText = str_replace('{FTN_ADDRESS}', $regData['ftn_address'], $welcomeText); $welcomeText = str_replace('{SYSTEM_NAME}', $systemName, $welcomeText); $welcomeText = str_replace('{NODE_TYPE}', $nodeType, $welcomeText); $welcomeText = str_replace('{AREAFIX_PASSWORD}', $regData['areafix_password'], $welcomeText); $welcomeText = str_replace('{POLLING_INFO}', $pollingInfo, $welcomeText); // Send welcome netmail (from sysop to themselves, appearing as from hub) $messageHandler = new MessageHandler(); $messageHandler->sendNetmail( $sysopUser['id'], // From user (sysop) $regData['ftn_address'], // To address (sysop's new address) $sysopName, // To name 'Welcome to LovlyNet!', // Subject $welcomeText, // Body 'LovlyNet Hub' // From name (appears as hub) ); echo "OK\n"; } } } catch (\Exception $e) { echo "SKIPPED\n"; echo "Note: " . $e->getMessage() . "\n"; } echo "\n"; echo str_repeat("=", 50) . "\n"; echo "LovlyNet setup complete!\n\n"; echo "Your FTN address is: {$regData['ftn_address']}\n"; if (!$isPublic) { echo "Node Type: Passive (poll-only)\n"; } echo "\nNext steps:\n"; echo " 1. Restart the admin daemon to pick up config changes\n"; echo " 2. Run a poll to connect to the hub: php scripts/binkp_poll.php\n"; echo " 3. Check the LovlyNet echo areas for messages\n"; if (!$isPublic) { echo "\nNote: As a passive node, you will need to poll the hub\n"; echo "regularly to send and receive mail. The hub cannot connect\n"; echo "to you directly. Consider setting up a cron job to poll\n"; echo "every 15-30 minutes.\n"; } echo "\n"; } // Main execution if ($argc > 1) { switch ($argv[1]) { case '--status': case 'status': showStatus(); exit(0); case '--update': case 'update': doRegistration(true); exit(0); case '--help': case 'help': case '-h': echo "LovlyNet Setup Tool\n"; echo "===================\n\n"; echo "Usage:\n"; echo " php lovlynet_setup.php Interactive registration\n"; echo " php lovlynet_setup.php --status Show registration status\n"; echo " php lovlynet_setup.php --update Update existing registration\n"; echo " php lovlynet_setup.php --help Show this help\n\n"; exit(0); default: echo "Unknown option: {$argv[1]}\n"; echo "Use --help for usage information.\n"; exit(1); } } doRegistration();