'audio/ogg', 'ogg' => 'audio/ogg', 'mp3' => 'audio/mpeg', 'caf' => 'audio/x-caf', 'wav' => 'audio/wav', ]); define('FTM_FILE_HASH_MAP', [ 'common' => [FTM_COMMON_CONFIG], 'sounds' => [FTM_SOUND_LIBRARY, FTM_SOUND_PACKS], 'sounds2' => [FTM_SOUND_LIBRARY, FTM_SOUND_PACKS], 'texttriggers' => [FTM_TEXT_TRIGGERS], 'soundtriggers' => [FTM_TEXT_TRIGGERS], ]); header('X-Powered-By: Futami'); header('Cache-Control: max-age=86400, must-revalidate'); header('Access-Control-Allow-Origin: *'); $reqMethod = (string)filter_input(INPUT_SERVER, 'REQUEST_METHOD'); if($reqMethod === 'OPTIONS') { http_response_code(204); header('Access-Control-Allow-Methods: OPTIONS, GET'); header('Access-Control-Allow-Headers: Cache-Control'); return; } if($reqMethod !== 'HEAD' && $reqMethod !== 'GET') { http_response_code(405); return; } function json_out($data): void { header('Content-Type: application/json; charset=utf-8'); echo json_encode($data); exit; } function ftm_hash_str(string $data): string { return hash(FTM_HASH, $data); } function ftm_hash_file(string $path): string { return hash_file(FTM_HASH, $path); } function hash_mapped_file(string $name): string { $files = FTM_FILE_HASH_MAP[$name] ?? []; if(count($files) === 1) return ftm_hash_file($files[0]); $data = ''; foreach($files as $file) $data .= file_get_contents($file); return ftm_hash_str($data); } function match_etag($eTag): void { if(filter_input(INPUT_SERVER, 'HTTP_IF_NONE_MATCH') === $eTag) { http_response_code(304); exit; } } function gen_etag(string $user, string $data): string { return sprintf('W/"%s-%s"', ftm_hash_str($data), $user); } function gen_etag_file(string $user, string $path): string { return gen_etag($user, ftm_hash_file($path)); } function etag(string $etag): void { match_etag($etag); header('ETag: ' . $etag); } function etag_data(string $user, string $data): void { etag(gen_etag($user, $data)); } function etag_file(string $user, string $path): void { etag(gen_etag_file($user, $path)); } $reqPath = '/' . trim(parse_url((string)filter_input(INPUT_SERVER, 'REQUEST_URI'), PHP_URL_PATH), '/'); if($reqPath === '/common.json') { etag_file('common', FTM_COMMON_CONFIG); $config = parse_ini_file(FTM_COMMON_CONFIG, false, INI_SCANNER_TYPED); $common = new stdClass; foreach($config as $key => $value) { if($key === 'colours') { $dict = $value; $value = []; foreach($dict as $name => $raw) $value[] = ['n' => $name, 'c' => $raw]; } elseif(is_string($value) && str_starts_with($value, '::')) { $parts = explode(':', substr($value, 2)); $hash = hash_mapped_file($parts[0]); $value = sprintf($parts[1], $hash); } $common->{$key} = $value; } json_out($common); } if(preg_match('#^/sounds2(\.[a-f0-9]{8})?.json$#', $reqPath)) { $sndLibData = file_get_contents(FTM_SOUND_LIBRARY); $sndPackData = file_get_contents(FTM_SOUND_PACKS); etag_data('sounds2', $sndLibData . $sndPackData); $sndLib = parse_ini_string($sndLibData, true, INI_SCANNER_TYPED); $sndPacks = parse_ini_string($sndPackData, true, INI_SCANNER_TYPED); $library = []; foreach($sndLib as $name => $info) $library[] = [ 'name' => $name, 'title' => $info['title'], 'sources' => $info['sources'], ]; $packs = []; foreach($sndPacks as $name => $info) $packs[] = [ 'name' => $name, 'title' => $info['title'], 'events' => $info['events'], ]; json_out(compact('library', 'packs')); } if(preg_match('#^/sounds(\.[a-f0-9]{8})?.json$#', $reqPath)) { $sndLibData = file_get_contents(FTM_SOUND_LIBRARY); $sndPackData = file_get_contents(FTM_SOUND_PACKS); etag_data('sounds', $sndLibData . $sndPackData); $sndLib = parse_ini_string($sndLibData, true, INI_SCANNER_TYPED); $sndPacks = parse_ini_string($sndPackData, true, INI_SCANNER_TYPED); $library = []; foreach($sndLib as $name => $info) { $sources = []; foreach($info['sources'] as $type => $path) { $sources[] = [ 'format' => FTM_LEGACY_SOUND_TYPE[$type], 'url' => $path, ]; } $library[] = [ 'id' => $name, 'name' => $info['title'], 'sources' => $sources, ]; } $packs = []; foreach($sndPacks as $name => $info) $packs[] = [ 'id' => $name, 'name' => $info['title'], 'events' => $info['events'], ]; json_out(compact('library', 'packs')); } if(preg_match('#^/texttriggers(\.[a-f0-9]{8})?.json$#', $reqPath)) { etag_file('texttriggers', FTM_TEXT_TRIGGERS); $triggers = parse_ini_file(FTM_TEXT_TRIGGERS, true, INI_SCANNER_TYPED); if($triggers === false) $triggers = []; else $triggers = array_values($triggers); json_out($triggers); } if(preg_match('#^/soundtriggers(\.[a-f0-9]{8})?.json$#', $reqPath)) { etag_file('soundtriggers', FTM_TEXT_TRIGGERS); $textTriggers = parse_ini_file(FTM_TEXT_TRIGGERS, true, INI_SCANNER_TYPED); $sndLib = parse_ini_file(FTM_SOUND_LIBRARY, true, INI_SCANNER_TYPED); $soundTrigs = []; foreach($textTriggers as $triggerInfo) { if($triggerInfo['type'] !== 'sound' && $triggerInfo['type'] !== 'alias') continue; $soundTrig = []; if($triggerInfo['type'] === 'sound') { $sounds = []; foreach($triggerInfo['sounds'] as $soundName) { if(!isset($sndLib[$soundName])) continue; $sound = []; $libSound = $sndLib[$soundName]; if(isset($libSound['sources']['mp3'])) $sound['m'] = $libSound['sources']['mp3']; if(isset($libSound['sources']['ogg'])) $sound['o'] = $libSound['sources']['ogg']; if(isset($libSound['sources']['opus'])) $sound['o'] = $libSound['sources']['opus']; if(isset($libSound['sources']['caf'])) $sound['c'] = $libSound['sources']['caf']; if(empty($sound)) continue; if(isset($triggerInfo['volume'])) { $sound['v'] = ceil(($triggerInfo['volume'] - 1) * 100); $sound['v2'] = $triggerInfo['volume']; } if(isset($triggerInfo['rate'])) $sound['r'] = $triggerInfo['rate']; $sounds[] = $sound; } $soundTrig['s'] = $sounds; } elseif($triggerInfo['type'] === 'alias') { $soundTrig['f'] = $triggerInfo['for']; } $matches = []; foreach($triggerInfo['match'] as $match) { $filters = []; $value = null; $notValue = null; $parts = explode(';', $match); foreach($parts as $part) { $part = explode(':', trim($part)); switch($part[0]) { case 'lc': $filters[] = 'lower'; break; case 'is': $filters[] = 'exact'; $value = trim($part[1]); break; case 'starts': $filters[] = 'starts'; $value = trim($part[1]); break; case 'has': $filters[] = 'contains'; $value = trim($part[1]); break; case 'hasnot': $notValue = trim($part[1]); break; default: $filters[] = 'missing:' . $part[0]; break; } } $matchNew = ['t' => implode(':', $filters)]; if($value !== null) $matchNew['m'] = $value; if($notValue !== null) $matchNew['n'] = $notValue; $matches[] = $matchNew; } $soundTrig['t'] = $matches; $soundTrigs[] = $soundTrig; } json_out([ 'meta' => [ 'baseUrl' => '', ], 'triggers' => $soundTrigs, ]); } if($reqPath === '/' || $reqPath === '/index.html' || $reqPath === '/index.php') { header('Content-Type: text/html; charset=utf-8'); echo << Data and settings shared between both chat clients is stored on this subdomain. HTML; return; } http_response_code(404);