server = $server; $this->active_users = $data['active_users']; $this->active_users_cutoff = $data['active_users_cutoff']; $this->name = $data['name']; $this->token = $data['token']; $this->admins = $data['admins']; $this->moderators = $data['moderators']; $this->created = $data['created']; $this->description = $data['description'] ?? ""; $this->image_id = $data['image_id']; $this->info_updates = $data['info_updates']; $this->message_sequence = $data['message_sequence']; $this->read = $data['read']; $this->write = $data['write']; $this->upload = $data['upload']; $room_identifier = $this->get_room_identifier(); $this->language_flag = isset($languages[$room_identifier]) ? $languages[$room_identifier] : ""; } /** * Create a CommunityRoom instance from data. * @param CommunityServer $server */ public static function from_data($server, array $data) { return new CommunityRoom($server, $data); } /** * Create an array of CommunityRoom instances from data. * @param array[] $data * @return CommunityRoom[] */ public static function from_data_array($server, array $data) { return array_map(function($room_data) use ($server) { return CommunityRoom::from_data($server, $room_data); }, $data); } function jsonSerialize(): array { $data = get_object_vars($this); unset($data['server']); return $data; } /** * Returns array of staff Session IDs. * @return string[] */ function get_staff() { return array_unique( [...$this->admins, ...$this->moderators] ); } /** * Returns seconds elapsed since room was created. */ function get_age(): float { return time() - $this->created; } function get_preview_url(): string { $base_url = $this->server->base_url; $token = $this->token; return "$base_url/r/$token"; } function get_invite_url(): string { $base_url = $this->server->base_url; $token = $this->token; return "$base_url/r/$token/invite.png"; } function get_join_url(): string { $base_url = $this->server->base_url; $pubkey = $this->server->pubkey; $token = $this->token; return "$base_url/$token?public_key=$pubkey"; } function get_icon_url(): string | bool { $image_id = $this->image_id; if ($image_id == null) return false; $base_url = $this->server->base_url; $token = $this->token; return "$base_url/room/$token/file/$image_id"; } /** * Returns our format of room identifier, * i.e. token+pubkey[:4] */ function get_room_identifier(): string { $token = $this->token; $pubkey_4 = substr($this->server->pubkey, 0, 4); return "$token+$pubkey_4"; } } $SERVER_STRINGIFY_MODE = 0; class CommunityServer implements JsonSerializable { // public static int $STRINGIFY_MODE = 0; public string $base_url = ""; public string $pubkey = ""; public ?array $rooms = null; /** * Instructs CommunityServer instances * to stringify to their base URL. */ private static function next_dedupe_by_url() { global $SERVER_STRINGIFY_MODE; $SERVER_STRINGIFY_MODE = 0; } /** * Instructs CommunityServer instances * to stringify to their public key. */ private static function next_dedupe_by_pubkey() { global $SERVER_STRINGIFY_MODE; $SERVER_STRINGIFY_MODE = 1; } /** * Filters the given servers to remove URL duplicates. * @param CommunityServer[] $servers * @return CommunityServer[] */ public static function dedupe_by_url($servers) { CommunityServer::next_dedupe_by_url(); return array_unique($servers); } /** * Filters the given servers to remove pubkey duplicates. * @param CommunityServer[] $servers * @return CommunityServer[] */ public static function dedupe_by_pubkey($servers) { CommunityServer::next_dedupe_by_pubkey(); return array_unique($servers); } private function __construct() { } /** * Compare two CommunityServer instances. * @param CommunityServer $a * @param CommunityServer $b */ static function compare($a, $b): int { return strcmp($a->base_url, $b->base_url); } /** * Sort an array of servers in-place based on URL. * @param CommunityServer[] &$servers */ static function sort(array &$servers) { usort($servers, 'CommunityServer::compare'); } /** * Stringify CommunityServer by custom property * to allow de-duping using array_uniq. */ function __toString(): string { global $SERVER_STRINGIFY_MODE; return match($SERVER_STRINGIFY_MODE) { 0 => $this->base_url, 1 => $this->pubkey }; } function jsonSerialize(): array { return get_object_vars($this); } /** * @return CommunityServer */ static function from_host($host) { $server = new CommunityServer(); $server->base_url = $host; return $server; } /** * @return CommunityServer */ static function from_known_host($host, $pubkey) { $server = new CommunityServer(); $server->base_url = $host; $server->pubkey = $pubkey; return $server; } /** * @return CommunityServer[] */ static function from_known_hosts($hosts, $pubkeys) { $servers = []; foreach ($hosts as $base_url) { $server = new CommunityServer(); $server->base_url = $base_url; $hostname = url_get_base($base_url, false); $server->pubkey = $pubkeys[$hostname]; $servers[] = $server; } return $servers; } /** * @return CommunityServer */ static function from_join_url(string $join_url) { $server = new CommunityServer(); $server->base_url = url_get_base($join_url); $server->set_pubkey_from_url($join_url); return $server; } /** * @return CommunityServer[] */ static function from_join_urls(array $join_urls) { return array_map( 'CommunityServer::from_join_url', $join_urls ); } /** * @param array $data * @return CommunityServer */ static function from_data(array $data) { $server = new CommunityServer(); $server->base_url = $data['base_url']; $server->pubkey = $data['pubkey']; $server->rooms = CommunityRoom::from_data_array($server, $data['rooms']); return $server; } /** * @param array[] $data * @return CommunityServer[] */ static function from_data_array(array $data) { return array_map( 'CommunityServer::from_data', $data ); } /** * @param CommunityServer[] $servers * @return CommunityRoom[] */ static function enumerate_rooms($servers) { $rooms = []; foreach ($servers as $server) { $rooms[] = $server->rooms; } return array_merge([], ...$rooms); } /** * Polls all servers for rooms. * @param CommunityServer[] $servers Servers to poll. * @return CommunityServer[] Reachable servers. */ public static function poll_reachable(array $servers): array { $reachable_servers = []; // Synchronous for-loop for now. foreach ($servers as $server) { if (!($server->fetch_rooms())) continue; // Accept failures to fetch pubkey if already known. // (Has happened.) if (!( $server->fetch_pubkey() || $server->has_pubkey() )) continue; $reachable_servers[] = $server; } return $reachable_servers; } function set_pubkey($pubkey) { if ($this->has_pubkey() && $this->pubkey != $pubkey) { $base_url = $this->base_url; throw new ValueError("Pubkey mismatch for $base_url"); } $this->pubkey = $pubkey; } function set_pubkey_from_url($join_url) { $url_components = parse_url($join_url); parse_str($url_components['query'], $query_components); $this->pubkey = $query_components['public_key']; } function has_pubkey() { return $this->pubkey != ""; } function is_reachable() { return url_is_reachable($this->base_url); } /** * Attempt to fetch rooms for self using SOGS API. * * @return bool True if successful, false otherwise. */ function fetch_rooms() { $base_url = $this->base_url; log_info("Fetching rooms for $base_url."); $rooms = curl_get_contents("$base_url/rooms?all=1"); if (!$rooms) { log_warning("Could not fetch rooms for $base_url."); return false; } $room_data = json_decode($rooms, true); if ($room_data == null) { log_warning("Could not parse rooms for $base_url."); return false; } $this->rooms = CommunityRoom::from_data_array($this, $room_data); return true; } /** * Attempt to fetch own public key by parsing SOGS HTML preview. * * @return bool True if successful, false otherwise. */ function fetch_pubkey() { if (empty($this->rooms)) { log_error("Server has no rooms to poll for public key"); throw new Error("Server has no rooms to poll for public key"); } $preview_url = $this->rooms[0]->get_preview_url(); log_info("Fetching pubkey from $preview_url"); $room_view = curl_get_contents($preview_url); if (!$room_view) { log_warning("Failed to fetch room preview from $preview_url."); return false; } $links = parse_join_links($room_view); if (!isset($links[0])) { log_warning("Could not locate join link in preview at $preview_url."); return false; } $this->set_pubkey_from_url($links[0]); return true; } } ?>