contents = $contents; } /** * Create new instance of SDIRCommunitySource. * Returns false if processing the source fails. * * @param string $contents Text from Session.directory to process. * * @return SDIRCommunitySource|false */ public static function from_contents(string $contents): SDIRCommunitySource | false { $source = new SDIRCommunitySource($contents); if (!$source->sdir_process_tags()) { return false; } return $source; } private readonly string $contents; /** * @var string[][] $tags Array associating room IDs with string tag arrays. */ private array $tags; private static function sdir_validate_entry( array $room_entry, bool &$missing_url, bool &$missing_tags, bool &$invalid_url ): bool { if (!isset($room_entry['url']) || !is_string($room_entry['url'])) { log_value($room_entry); $missing_url = true; return false; } if (!isset($room_entry['tags']) || !is_string($room_entry['tags'])) { log_value($room_entry); $missing_tags = true; return false; } if (!filter_var($room_entry['url'], FILTER_VALIDATE_URL)) { log_value($room_entry); $invalid_url = true; return false; } return true; } private static function sdir_report_errors(bool $entry_missing_url, bool $entry_missing_tags, bool $entry_invalid_url) { if ($entry_missing_url) { log_error("One or more room entries from session.directory is missing the 'url' parameter."); } if ($entry_missing_tags) { log_error("One or more room entries from session.directory is missing the 'tags' parameter."); } if ($entry_invalid_url) { log_warning("One or more room entries from session.directory contain an invalid 'url' parameter."); } } private function get_sdir_entries(): array|bool { try { return json_decode($this->contents, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { return false; } } private function sdir_process_tags(): bool { $entry_missing_url = false; $entry_missing_tags = false; $entry_invalid_url = false; $rooms = SDIRCommunitySource::get_sdir_entries(); if (!$rooms) { log_error("Could not parse entries from session.directory."); return false; } foreach ($rooms as $room_entry) { if (!SDIRCommunitySource::sdir_validate_entry( $room_entry, $entry_missing_url, $entry_missing_tags, $entry_invalid_url )) { continue; } $url = $room_entry['url']; $tag_string = $room_entry['tags']; $nsfw = isset($room_entry['nsfw']) && $room_entry['nsfw'] == "1"; $room_id = url_get_room_id($url); $this->tags[$room_id] = explode(',', $tag_string); if ($nsfw) { $this->tags[$room_id] = array_values(array_unique([ "nsfw", ...$this->tags[$room_id] ])); } } SDIRCommunitySource::sdir_report_errors($entry_missing_url, $entry_missing_tags, $entry_invalid_url); return true; } /** * Produce an array of string tags for all Communities found. * @return string[][] Array associating room IDs with string tag arrays. */ public function get_tags(): array { return $this->tags; } } /** * Parses Communities fetched from [Awesome-Session-Group-List](https://raw.githubusercontent.com/GNU-Linux-libre/Awesome-Session-Group-List/main/README.md). */ class ASGLCommunitySource implements CommunitySourceWithTags { private function __construct(string $contents) { $this->contents = $contents; } private readonly string $contents; /** * @var string[][] $tags; */ private array $tags = []; /** * Attempt to create an ASGLCommunitySource instance. * * Returns false if processing the source fails. * * @param string $contents Text from ASGL to process. * * @return ASGLCommunitySource|false */ public static function from_contents(string $contents): ASGLCommunitySource | false { $source = new ASGLCommunitySource($contents); if(!$source->asgl_process_tags()) { return false; } return $source; } private function asgl_process_tags(): bool { $lines = explode("\n", $this->contents); // $last_headings = []; $last_room_id = null; $room_tags = []; foreach ($lines as $line) { ASGLCommunitySource::asgl_process_line($line, $last_room_id, $room_tags); if ($last_room_id != null && count($room_tags) > 0) { $this->tags[$last_room_id] = $room_tags; $last_room_id = null; $room_tags = []; } } return true; } private static function asgl_process_line( ?string $line, ?string &$last_room_id, array &$room_tags ) { $line = trim($line); if (strlen($line) == 0) { $last_room_id = null; return; } $urls = parse_join_links($line); if (count($urls) == 1 && $urls[0] == $line) { $last_room_id = url_get_room_id($urls[0]); $room_tags = []; return; } if (str_starts_with($line, "hashtag")) { $room_tags = ASGLCommunitySource::read_asgl_tags($line); } } private static function read_asgl_tags(string $line): array { $matches = []; preg_match_all('/`#([^`]+)`/', $line, $matches); // Return first group matches. return $matches[1]; } /** * Produce an array of string tags for all Communities found. * @return string[][] Array associating room IDs with string tag arrays. */ public function get_tags(): array { return $this->tags; } } /** * Finds Communities on the web. */ class CommunitySources { private const SOURCES = array( 'ASGL' => 'https://raw.githubusercontent.com/GNU-Linux-libre/Awesome-Session-Group-List/main/README.md', 'LOKI' => 'https://lokilocker.com/Mods/Session-Groups/wiki/Session-Open-Groups', 'SDIR-JSON' => 'https://session.directory/scrape.php', 'SIMP' => 'https://simplifiedprivacy.com/techgroups', 'FARK' => 'https://freearkham.cc/', // Unused 'SDIR' => 'https://session.directory/?all=groups', 'SDIR-PATTERN' => '/view_session_group_user_lokinet\.php\?id=\d+/', 'SDIR-BASE' => 'https://session.directory/', ); private readonly string $contents_asgl; private readonly string $contents_loki; private readonly string $contents_sdir; private readonly string $contents_fark; private readonly string $contents_simp; private readonly string $contents_aggregated; /** * Arraying associating room identifiers with arrays of raw tags. * @var array $room_tags */ private array $room_tags = []; /** * Creates a new CommunitySources instance with processed Community data from the Web. */ public function __construct() { log_info("Requesting Awesome Session Group list..."); $this->contents_asgl = CommunitySources::fetch_source('ASGL'); log_info("Requesting Lokilocker Mods Open Group list..."); $this->contents_loki = CommunitySources::fetch_source('LOKI'); log_info("Requesting session.directory list..."); $this->contents_sdir = CommunitySources::fetch_source('SDIR-JSON'); log_info("Requesting FreeArkham.cc list..."); $this->contents_fark = CommunitySources::fetch_source('FARK'); log_info("Requesting SimplifiedPrivacy.com list..."); $this->contents_simp = CommunitySources::fetch_source('SIMP'); log_info("Parsing extra information..."); if (!$this->process_sources()) { log_error("Could not parse extra information from one or more sources."); } log_info('Done fetching sources.'); $this->contents_aggregated = $this->contents_asgl . $this->contents_fark . $this->contents_loki . $this->contents_simp . // Slashes are escaped when served, unescape them str_replace("\\/", "/", $this->contents_sdir); } private static function source_cache_file(string $source_key) { global $SOURCES_CACHE; return "$SOURCES_CACHE/$source_key"; } private static function fetch_source(string $source_key) { $url = CommunitySources::SOURCES[$source_key]; $contents = file_get_contents($url); log_debug($http_response_header[0]); $cache_file = CommunitySources::source_cache_file($source_key); if ($contents) { file_put_contents($cache_file, $contents); return $contents; } $contents = file_get_contents($cache_file); if ($contents) { log_warning("Could not fetch source from $url, using cache"); return $contents; } log_error("Could not fetch source from $url."); return ""; } /** * @param string[][] $tags Array associating room IDs to tag arrays */ private function add_tags(array $tags) { foreach ($tags as $room_id => $room_tags) { if (!isset($this->room_tags[$room_id])) { $this->room_tags[$room_id] = []; } $this->room_tags[$room_id] = [ ...$this->room_tags[$room_id], ...$room_tags ]; } } private function process_sources(): bool { $source_sdir = SDIRCommunitySource::from_contents($this->contents_sdir); $source_asgl = ASGLCommunitySource::from_contents($this->contents_asgl); $source_sdir && $this->add_tags($source_sdir->get_tags()); $source_asgl && $this->add_tags($source_asgl->get_tags()); if (!$source_sdir) { return false; } if (!$source_asgl) { return false; } return true; } /** * Return all known join links to Session Communities. * @return string[] Join URLs. */ public function get_join_urls(): array { return array_unique( parse_join_links($this->contents_aggregated) ); } /** * Return all known tags for the given room. * @param string $room_id Room identifier. * @return CommunityTag[] Array of string tags. */ public function get_room_tags($room_id): array { if (!isset($this->room_tags[$room_id])) { return []; } return $this->room_tags[$room_id]; } } file_exists($SOURCES_CACHE) or mkdir($SOURCES_CACHE, 0755, recursive: true); ?>