PHP Classes

File: scripts/matterbridge_daemon.php

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   scripts/matterbridge_daemon.php   Download  
File: scripts/matterbridge_daemon.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: 7,454 bytes
 

Contents

Class file image Download
#!/usr/bin/env php
<?php

/*
 * Matterbridge Inbound Daemon
 *
 * Polls the Matterbridge API for inbound messages (e.g. from Discord) and
 * injects them into the appropriate local chat rooms.
 *
 * Usage: php matterbridge_daemon.php [--daemon] [--pid-file=path] [--log-level=LEVEL] [--poll-interval=N]
 */

chdir(__DIR__ . "/../");

require_once
__DIR__ . '/../vendor/autoload.php';
require_once
__DIR__ . '/../src/functions.php';

use
BinktermPHP\Binkp\Logger;
use
BinktermPHP\Chat\ChatMessageService;
use
BinktermPHP\Chat\MatterbridgeConfig;
use
BinktermPHP\Chat\MatterbridgeService;
use
BinktermPHP\Database;

// ========================================
// CLI Helpers
// ========================================

function showUsage(): void
{
    echo
"Usage: php matterbridge_daemon.php [options]\n";
    echo
"Options:\n";
    echo
" --daemon Run as daemon (detach from terminal)\n";
    echo
" --pid-file=FILE Write PID file (default: data/run/matterbridge_daemon.pid)\n";
    echo
" --log-level=LEVEL Log level (default: INFO)\n";
    echo
" --poll-interval=N Seconds between polls (default: 2)\n";
    echo
" --help Show this help message\n\n";
}

function
parseArgs(array $argv): array
{
   
$args = [];
    foreach (
$argv as $arg) {
        if (
strpos($arg, '--') === 0) {
            if (
strpos($arg, '=') !== false) {
                [
$key, $value] = explode('=', substr($arg, 2), 2);
               
$args[$key] = $value;
            } else {
               
$args[substr($arg, 2)] = true;
            }
        }
    }
    return
$args;
}

function
daemonize(): void
{
   
$pid = pcntl_fork();
    if (
$pid === -1) {
        die(
"Could not fork process\n");
    } elseif (
$pid > 0) {
        echo
"Matterbridge daemon started with PID: $pid\n";
        exit(
0);
    }

    if (
posix_setsid() === -1) {
        die(
"Could not detach from terminal\n");
    }

   
chdir('/');
   
umask(0);
   
fclose(STDIN);
   
fclose(STDOUT);
   
fclose(STDERR);
}

// ========================================
// Main
// ========================================

$args = parseArgs($argv);

if (isset(
$args['help'])) {
   
showUsage();
    exit(
0);
}

$pidFile = $args['pid-file'] ?? 'data/run/matterbridge_daemon.pid';
$logLevelName = strtoupper((string)($args['log-level'] ?? 'INFO'));
$pollInterval = max(1, (int)($args['poll-interval'] ?? 2));

// Resolve to absolute path before daemonize() calls chdir('/').
if (!str_starts_with($pidFile, '/')) {
   
$pidFile = getcwd() . '/' . $pidFile;
}

$logLevelMap = [
   
'DEBUG' => Logger::LEVEL_DEBUG,
   
'INFO' => Logger::LEVEL_INFO,
   
'WARNING' => Logger::LEVEL_WARNING,
   
'ERROR' => Logger::LEVEL_ERROR,
   
'CRITICAL' => Logger::LEVEL_CRITICAL,
];
$logLevel = $logLevelMap[$logLevelName] ?? Logger::LEVEL_INFO;

if (isset(
$args['daemon'])) {
   
daemonize();
}

$pidDir = dirname($pidFile);
if (!
is_dir($pidDir)) {
   
mkdir($pidDir, 0755, true);
}
file_put_contents($pidFile, getmypid() . "\n");

$logger = new Logger(
    \
BinktermPHP\Config::getLogPath('server.log'),
   
$logLevel,
   
false
);

$logger->info('Matterbridge daemon starting', [
   
'poll_interval' => $pollInterval,
   
'pid_file' => $pidFile,
]);

// ========================================
// Signal handling
// ========================================

$running = true;

if (
function_exists('pcntl_signal')) {
   
pcntl_signal(SIGTERM, function () use (&$running, $logger) {
       
$logger->info('Matterbridge daemon received SIGTERM, shutting down');
       
$running = false;
    });
   
pcntl_signal(SIGINT, function () use (&$running, $logger) {
       
$logger->info('Matterbridge daemon received SIGINT, shutting down');
       
$running = false;
    });
}

// ========================================
// Room cache (gateway name -> room id, refreshed every 60 s)
// ========================================

/** @var array<string,int> $roomCache gateway => room_id */
$roomCache = [];
$roomCacheExpiry = 0;

function
refreshRoomCache(\PDO $db): array
{
   
$stmt = $db->query("
        SELECT id, matterbridge_gateway
        FROM chat_rooms
        WHERE matterbridge_enabled = true
          AND matterbridge_gateway IS NOT NULL
          AND matterbridge_gateway <> ''
    "
);
   
$cache = [];
    foreach (
$stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
       
$cache[(string)$row['matterbridge_gateway']] = (int)$row['id'];
    }
    return
$cache;
}

// ========================================
// Main loop
// ========================================

$config = MatterbridgeConfig::getInstance();
$service = new MatterbridgeService($config, $logger);

if (!
$config->isEnabled()) {
   
$logger->warning('Matterbridge is not enabled in config/matterbridge.json ? exiting');
    @
unlink($pidFile);
    exit(
0);
}

$bridgeUserId = $config->getBridgeUserId();
if (
$bridgeUserId <= 0) {
   
$logger->warning('bridge_user_id is not set in config/matterbridge.json ? inbound messages cannot be injected');
   
$logger->warning('Create a dedicated bridge user, note its ID, and set bridge_user_id in config/matterbridge.json');
    @
unlink($pidFile);
    exit(
1);
}

$db = Database::getInstance()->getPdo();
$chatService = new ChatMessageService($db, $service, $logger);

$logger->info('Matterbridge daemon ready', [
   
'bridge_user_id' => $bridgeUserId,
   
'base_url' => $config->getBaseUrl(),
]);

while (
$running) {
    if (
function_exists('pcntl_signal_dispatch')) {
       
pcntl_signal_dispatch();
    }

   
// Refresh room cache every 60 seconds
   
if (time() >= $roomCacheExpiry) {
       
$roomCache = refreshRoomCache($db);
       
$roomCacheExpiry = time() + 60;

       
$logger->debug('Room cache refreshed', ['gateways' => array_keys($roomCache)]);
    }

    try {
       
$messages = $service->pollMessages();
    } catch (\
Throwable $e) {
       
$logger->error('Matterbridge poll failed', ['error' => $e->getMessage()]);
       
sleep($pollInterval);
        continue;
    }

    foreach (
$messages as $msg) {
       
$gateway = trim((string)($msg['gateway'] ?? ''));
       
$text = trim((string)($msg['text'] ?? ''));
       
$username = trim((string)($msg['username'] ?? ''));
       
$protocol = strtolower(trim((string)($msg['protocol'] ?? 'remote')));

        if (
$gateway === '' || $text === '' || $username === '') {
            continue;
        }

       
$roomId = $roomCache[$gateway] ?? null;
        if (
$roomId === null) {
           
$logger->debug('No local room for gateway, skipping', [
               
'gateway' => $gateway,
            ]);
            continue;
        }

       
$body = '[' . strtoupper($protocol) . '] <' . $username . '> ' . $text;

        try {
           
$chatService->sendMessage($bridgeUserId, $roomId, null, $body, false);
           
$logger->debug('Injected inbound bridge message', [
               
'gateway' => $gateway,
               
'room_id' => $roomId,
               
'protocol' => $protocol,
               
'username' => $username,
            ]);
        } catch (\
Throwable $e) {
           
$logger->error('Failed to inject inbound bridge message', [
               
'gateway' => $gateway,
               
'room_id' => $roomId,
               
'error' => $e->getMessage(),
            ]);
        }
    }

   
sleep($pollInterval);
}

$logger->info('Matterbridge daemon stopped');
@
unlink($pidFile);