You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sessioncommunities.online/php/servers/servers-rooms.php

1142 lines
32 KiB
PHP

<?php
require_once 'languages/language-flags.php';
require_once 'servers/known-servers.php';
require_once 'servers/tags.php';
require_once 'utils/fetching-coroutines.php';
require_once 'assets/room-icons.php';
require_once 'assets/room-invites.php';
$MINUTE_SECONDS = 60;
$HOUR_SECONDS = 60 * $MINUTE_SECONDS;
$DAY_SECONDS = 24 * $HOUR_SECONDS;
$WEEK_SECONDS = 7 * $DAY_SECONDS;
/**
* Representation of Session Community room.
*/
class CommunityRoom implements JsonSerializable {
/** @var CommunityServer $server Server this room belongs to. */
public readonly object $server;
/** @var ?int $active_users Number of active users in the defined period. */
public readonly ?int $active_users;
/** @var ?int $active_users_cutoff Period for `$active_users`, in seconds. */
public readonly ?int $active_users_cutoff;
/** @var string $token Room name in Community API. */
public readonly string $token;
/** @var ?string $name User-facing name given to Community. */
public readonly ?string $name;
/** @var ?string[] $admins The mixed Session IDs of public room admins. */
public readonly ?array $admins;
/** @var ?string[] $moderators The mixed Session IDs of public room moderators. */
public readonly ?array $moderators;
/** @var ?float $created UNIX timestamp of room creation, in seconds. */
public readonly ?float $created;
/** @var ?string $description User-facing description given to Community. */
public readonly ?string $description;
/** @var ?int $image_id Optional file ID for this room's icon, as served under the room files. */
public readonly ?int $image_id;
/** @var ?int $info_updates Monotonic integer counter that increases
* whenever the room's metadata changes. */
public readonly ?int $info_updates;
/** @var ?int $message_sequence Monotonic room post counter that
* increases each time a message is posted, edited, or deleted in this room. */
public readonly ?int $message_sequence;
/**
* @var ?bool $read
* This boolean flag indicates whether a regular user
* has permission to read messages in this room.
*/
public readonly ?bool $read;
/**
* @var ?bool $upload
* This boolean flag indicates whether a regular user
* has permission to upload files to this room.
*/
public readonly ?bool $upload;
/**
* @var ?bool $write
* This boolean flag indicates whether a regular user
* has permission to write messages to this room.
*/
public readonly ?bool $write;
// Custom properties
/**
* @var string $language_flag
* Optional Unicode emoji of region matching
* the primary language of this room.
*
* Custom attribute.
*/
public readonly ?string $language_flag;
/**
* @var string[] $tags
* String tags applied to room by creator or submitter.
*
* Custom attribute.
*/
private array $tags = [];
private function __construct(\CommunityServer $server, array $details) {
global $languages;
$this->server = $server;
$this->active_users = $details['active_users'];
$this->active_users_cutoff = $details['active_users_cutoff'];
$this->name = $details['name'];
$this->token = $details['token'];
$this->admins = $details['admins'];
$this->moderators = $details['moderators'];
$this->created = $details['created'];
$this->description = $details['description'] ?? "";
$this->image_id = $details['image_id'];
$this->info_updates = $details['info_updates'];
$this->message_sequence = $details['message_sequence'];
$this->read = $details['read'];
$this->write = $details['write'];
$this->upload = $details['upload'];
$room_identifier = $this->get_room_identifier();
$this->language_flag =
isset($languages[$room_identifier])
? $languages[$room_identifier]
: "";
}
/**
* Return information for JSON serialization.
*/
function jsonSerialize(): array {
$details = get_object_vars($this);
unset($details['server']);
$details['tags'] = $this->get_raw_tags();
return $details;
}
/**
* Return information for JSON serialization in listing.
*/
function to_listing_data(): array {
$details = get_object_vars($this);
unset($details['server']);
unset($details['tags']);
unset($details['language_flag']);
return array(
"room" => $details,
"room_extra" => array(
"join_url" => $this->get_join_url(),
"language_flag" => $this->language_flag,
"tags" => $this->get_raw_tags()
)
);
}
/**
* Create a CommunityRoom instance from loaded data.
* @param CommunityServer $server
*/
public static function from_details($server, array $details) {
$room = new CommunityRoom($server, $details);
$has_tags = isset($details['tags']);
if ($has_tags) {
$room->tags = $details['tags'];
}
return $room;
}
/**
* Create an array of CommunityRoom instances from loaded data.
* @param array[] $details
* @return CommunityRoom[]
*/
public static function from_details_array($server, array $details) {
return array_map(function($room_data) use ($server) {
return CommunityRoom::from_details($server, $room_data);
}, $details);
}
/**
* Sorts Community rooms in-place by the given string property.
* @param \CommunityRoom[] $rooms Rooms to sort by given key.
* @param string $key String property of CommunityRoom to sort by.
*/
public static function sort_rooms_str(array &$rooms, string $key) {
usort($rooms, function(\CommunityRoom $a, \CommunityRoom $b) use ($key) {
return strcmp(
$a->$key,
$b->$key
);
});
}
/**
* Sorts Community rooms in-place by their server's public key.
* @param \CommunityRoom[] $rooms Rooms to sort by server pubkey.
*/
public static function sort_rooms_by_pubkey(array &$rooms) {
usort($rooms, function(\CommunityRoom $a, \CommunityRoom $b) {
return strcmp(
$a->server->get_pubkey(),
$b->server->get_pubkey()
);
});
}
/**
* 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;
}
/**
* Formats the period over which active users are counted as a duration string.
* @return string Active user cutoff period for this room, expressed in days.
*/
function format_user_cutoff_period(): ?string {
global $WEEK_SECONDS, $DAY_SECONDS, $HOUR_SECONDS, $MINUTE_SECONDS;
$active_users_cutoff = $this->active_users_cutoff;
if ($active_users_cutoff >= $WEEK_SECONDS) {
return floor($active_users_cutoff / $WEEK_SECONDS) . ' week(s)';
}
if ($active_users_cutoff >= $DAY_SECONDS) {
return floor($active_users_cutoff / $DAY_SECONDS) . ' day(s)';
}
if ($active_users_cutoff >= $HOUR_SECONDS) {
return floor($active_users_cutoff / $HOUR_SECONDS) . ' hour(s)';
}
if ($active_users_cutoff >= $MINUTE_SECONDS) {
return floor($active_users_cutoff / $MINUTE_SECONDS) . 'minute(s)';
}
return floor($active_users_cutoff) . 's';
}
/**
* Return the browser preview URL for this room.
*/
function get_preview_url(): string {
$base_url = $this->server->base_url;
$token = $this->token;
return "$base_url/r/$token";
}
/**
* Return the QR code invite URL for this room.
*/
function get_invite_url(): string {
$base_url = $this->server->base_url;
$token = $this->token;
return "$base_url/r/$token/invite.png";
}
/**
* Return the in-app join URL for this room.
*/
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";
}
/**
* Return the URL of this room's designated icon.
*/
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";
}
/**
* Return our format of room identifier.
* @return string String in the form `token+pubkey[:4]`.
*/
function get_room_identifier(): string {
$token = $this->token;
$pubkey_4 = substr($this->server->pubkey, 0, 4);
return "$token+$pubkey_4";
}
/**
* Checks whether this room belongs to a Session-owned server.
*/
function is_official_room(): bool {
return $this->server->is_official_server();
}
/**
* @param string[] $tags
*/
public function add_tags(array $tags) {
$this->tags = [...$this->tags, ...$tags];
}
public function has_nsfw_keywords(): bool {
// Description not included due to false positives.
$blob =
strtolower($this->name) . " " .
strtolower(join(" ", $this->tags));
foreach (CommunityTag::NSFW_KEYWORDS as $keyword) {
if (str_contains($blob, $keyword)) {
return true;
}
}
return false;
}
public const USERS_PER_STAFF = 50;
public const USERS_PER_STAFF_WARNING = 200;
private function has_good_staff_rating(): bool {
$recommended_staff_count = $this->active_users / CommunityRoom::USERS_PER_STAFF;
return count($this->get_staff()) >= $recommended_staff_count;
}
private function has_poor_staff_rating(): bool {
if ($this->active_users <= 3) {
return false;
}
$minimal_staff_count = $this->active_users / CommunityRoom::USERS_PER_STAFF_WARNING;
return count($this->get_staff()) < $minimal_staff_count;
}
/**
* @return string[]
*/
function get_raw_tags(): array {
return array_unique(array_values($this->tags));
}
/**
* Return the tags associated with this room.
* @return \CommunityTag[] Tags as string array.
*/
function get_room_tags(): array {
global $ROOMS_USED_BY_PROJECT;
$user_tags = CommunityTag::from_user_tags($this->tags, remove_redundant: true);
/**
* @var \CommunityTag[] $derived_tags
*/
$derived_tags = [];
$CHECK_MARK = CommunityTag::CHECK_MARK;
$WARNING = CommunityTag::WARNING_ICON;
$USERS_PER_STAFF = CommunityRoom::USERS_PER_STAFF;
$USERS_PER_STAFF_WARNING = CommunityRoom::USERS_PER_STAFF_WARNING;
if ($this->is_official_room()) {
$derived_tags[] = new CommunityTag(
"official",
TagType::RESERVED_TAG,
"This Community is maintained by the Session team. $CHECK_MARK"
);
}
if ($this->has_nsfw_keywords()) {
$derived_tags[] =
new CommunityTag(
"nsfw",
TagType::WARNING_TAG,
"This Community may contain adult material. $WARNING"
);
}
if ($this->write && $this->has_good_staff_rating()) {
$derived_tags[] =
new CommunityTag(
"modded",
TagType::RESERVED_TAG,
"This Community has at least 1 staff per $USERS_PER_STAFF active users. $CHECK_MARK"
);
}
/*
if ($this->write && $this->has_poor_staff_rating()) {
$derived_tags[] =
new CommunityTag(
"not modded",
TagType::WARNING_TAG,
"This Community has less than 1 staff per $USERS_PER_STAFF_WARNING active users. $WARNING"
);
}
*/
if (!$this->write) {
$derived_tags[] =
new CommunityTag(
"read-only",
TagType::RESERVED_TAG,
"This Community is read-only."
);
}
if ($this->write && !$this->upload) {
$derived_tags[] =
new CommunityTag(
"uploads off",
TagType::RESERVED_TAG,
"This Community does not support uploading files or link previews."
);
}
if ($this->created && $this->created > strtotime("-4 week")) {
$derived_tags[] =
new CommunityTag(
"new",
TagType::RESERVED_TAG,
"This Community was created recently."
);
}
if (in_array($this->get_room_identifier(), $ROOMS_USED_BY_PROJECT)) {
$derived_tags[] =
new CommunityTag(
"we're here",
TagType::RESERVED_TAG,
"The sessioncommunities.online maintainer(s) can post updates "
. "or respond to feedback in this Community."
);
}
return [...$derived_tags, ...$user_tags];
}
}
/**
* Class representing Session Community server hosting Community rooms.
*/
class CommunityServer implements JsonSerializable {
/** @var string $base_url The root URL of this server. */
public string $base_url = "";
/** @var string $pubkey The SOGS protocol pubkey of this server. */
public string $pubkey = "";
/** @var ?\CommunityRoom[] Array of rooms hosted by this server. */
public ?array $rooms = null;
/**
* @var string[] $room_hints
* This array contains fallback room tokens collected from links.
* Used only if fetching rooms list fails.
*/
private array $room_hints = [];
/**
* @var bool $merge_error
*
* Flag specifying whether the server is invalidated as a result of merging.
*/
private bool $merge_error = false;
private function __construct() {}
/**
* Compare two CommunityServer instances by base URL.
* @param CommunityServer $a First server to compare URLs.
* @param CommunityServer $b Second server to compare URLs.
* @return int A number less than, equal to, or greater than zero
* when the servers are in correct order, interchangable, or in reverse order,
* respectively.
*/
static function compare_by_url($a, $b): int {
return strcmp(
$a->get_hostname(),
$b->get_hostname()
);
}
/**
* Sort an array of servers in place based on URL.
* @param CommunityServer[] &$servers
*/
static function sort_by_url(array &$servers) {
usort($servers, 'CommunityServer::compare_by_url');
}
/**
* Compare two CommunityServer instances by public key.
* @param CommunityServer $a First server to compare public keys.
* @param CommunityServer $b Second server to compare public keys.
* @return int A number less than, equal to, or greater than zero
* when the servers are in correct order, interchangable, or in reverse order,
* respectively.
*/
static function compare_by_pubkey($a, $b): int {
return strcmp($a->pubkey, $b->pubkey);
}
/**
* Sorts an array of servers in place by public key.
* @param CommunityServer[] $servers
*/
public static function sort_by_pubkey(&$servers) {
usort($servers, 'CommunityServer::compare_by_pubkey');
}
/**
* Absorbs extra info from another instance of the same server.
* @param CommunityServer $server
*
* @return True if successful, false in case of mistmatch.
*/
private function merge_from($server): bool {
// Merge room hint information.
$this->room_hints = [
...$this->room_hints,
...$server->room_hints
];
// Merge public key information.
// In case of error, set the `merge_error` flag.
if (!$this->set_pubkey($server->pubkey)) {
if ($this->merge_error) {
return false;
}
$base_url = $this->base_url;
$other_base_url = $server->base_url;
$pubkey_old = $this->pubkey;
$pubkey_new = $server->pubkey;
log_error(
"Key collision for $base_url:" .
"Have $pubkey_old, fetched $pubkey_new" .
"from server $other_base_url"
);
$this->merge_error = true;
return false;
}
// Prefer HTTPS URLs over HTTP.
if (str_starts_with($server->base_url, "https:")) {
$this->base_url = $server->get_scheme() . "://" . $this->get_hostname();
}
// Prefer domain names over IPs (connections to SOGS survive relocation).
if (filter_var($this->get_hostname(), FILTER_VALIDATE_IP)) {
$this->base_url = $this->get_scheme() . "://" . $server->get_hostname();
}
return true;
}
/**
* Re-introduces the servers to a consistent state after merging.
* @param CommunityServer[] $servers
* @return CommunityServer[]
*/
private static function ensure_merge_consistency(array $servers) {
// Exclude servers with merge errors.
$servers = array_filter($servers, function(\CommunityServer $server) {
return !$server->merge_error;
});
// Remove duplicate room hints; does not require sorting.
foreach ($servers as $server) {
$server->room_hints = array_unique($server->room_hints);
}
return $servers;
}
/**
* Merges consecutive servers in array in place on equality of given attribute.
* @param CommunityServer[] $servers Servers sorted by given attribute.
* @param string $method Method name to retrieve attribute from server.
*/
private static function merge_by(&$servers, string $method) {
// Backwards-merging to preserve indexing for unprocessed servers.
// Merging only makes sense for pairs, so stop at $i = 1.
for ($i = count($servers) - 1; $i >= 1; $i--) {
if ($servers[$i]->$method() == $servers[$i - 1]->$method()) {
// Merge this server into the previous one, discarding it.
$servers[$i - 1]->merge_from($servers[$i]);
array_splice($servers, $i, 1);
}
}
}
/**
* Write details about this server to debug log.
*/
private function log_details() {
$base_url = $this->base_url;
$count_rooms = count($this->rooms ?? []);
$count_room_hints = count($this->room_hints);
$pubkey = $this->pubkey ? truncate($this->pubkey, 4) : "unknown";
log_debug("Server $base_url"."[$count_rooms/$count_room_hints] { pubkey: $pubkey }");
}
/**
* Filters the given servers to remove URL duplicates.
* @param CommunityServer[] $servers Servers to merge by URL.
* @return CommunityServer[] Merged URL-unique servers.
*/
public static function dedupe_by_url($servers) {
CommunityServer::sort_by_url($servers);
CommunityServer::merge_by($servers, "get_hostname");
$servers = CommunityServer::ensure_merge_consistency($servers);
return $servers;
}
/**
* Filters the given servers to remove pubkey duplicates.
* @param CommunityServer[] $servers Servers to merge by public key.
* @return CommunityServer[] Merged pubkey-unique servers.
*/
public static function dedupe_by_pubkey($servers) {
CommunityServer::sort_by_pubkey($servers);
CommunityServer::merge_by($servers, "get_pubkey");
$servers = CommunityServer::ensure_merge_consistency($servers);
return $servers;
}
/**
* Return information for JSON serialization.
*/
function jsonSerialize(): array {
$details = get_object_vars($this);
unset($details['room_hints']);
unset($details['merge_error']);
return $details;
}
/**
* Create server instances located on hardcoded hosts.
* @param string[] $hosts Array of base URLs for known servers.
* @param string[] $pubkeys
* Associative array from hostnames to SOGS public keys.
* @return CommunityServer[] Array of resulting Community servers.
*/
static function from_known_hosts(array $hosts, array $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;
}
/**
* Create server instances from given room join URLs.
* Resulting servers will know of the embedded room tokens.
* @param string[] $join_urls Join URLs found in the wild.
* @return CommunityServer[] Array of resulting Community servers.
*/
static function from_join_urls(array $join_urls) {
$servers = [];
foreach ($join_urls as $join_url) {
$server = new CommunityServer();
// Call must succeed with no default public key.
$server->initialize_from_url($join_url);
$servers[] = $server;
}
return $servers;
}
/**
* Create Community server instance from loaded server data.
* @param array $details Decoded JSON associative data about server.
* @return CommunityServer Server represented by given data.
*/
static function from_details(array $details) {
$server = new CommunityServer();
$server->base_url = $details['base_url'];
$server->pubkey = $details['pubkey'];
$server->rooms = CommunityRoom::from_details_array($server, $details['rooms']);
return $server;
}
/**
* Create Community server instance from array loaded server data.
* @param array $details Decoded JSON associative arrays about server.
* @return CommunityServer[] Servers represented by given data.
*/
static function from_details_array(array $details_array) {
$servers = [];
foreach ($details_array as $details) {
$servers[] = CommunityServer::from_details($details);
}
return $servers;
}
/**
* Add to the given servers additional data extracted from our sources.
* @param \CommunityServer[] $servers
* @param \CommunitySources $source
*/
static function source_additional_info(array $servers, \CommunitySources $source): void {
foreach ($servers as $server) {
foreach ($server->rooms as $room) {
$sourced_tags = $source->get_room_tags($room->get_room_identifier());
$room->add_tags($sourced_tags);
}
}
}
/**
* Collect the rooms among the given Community servers.
* @param CommunityServer[] $servers Array of Community servers.
* @return CommunityRoom[]
* Array of all rooms contained in the given servers.
*/
static function enumerate_rooms($servers) {
$rooms = [];
foreach ($servers as $server) {
$rooms[] = $server->rooms;
}
return array_merge([], ...$rooms);
}
/**
* Polls given servers for rooms and public key and saves this info.
* Servers will be disqualified if no rooms can be found,
* and/or if no public key is obtained or hardcoded.
* @param CommunityServer[] $servers Servers to fetch.
* @return CommunityServer[] Servers polled successfully.
*/
public static function poll_reachable(array $servers): array {
$reachable_servers = [];
foreach ($servers as $server) {
$fetch_job = function() use ($server, &$reachable_servers): Generator {
if (!yield from $server->fetch_rooms_coroutine()) return;
if (!yield from $server->fetch_pubkey_coroutine()) return;
$reachable_servers[] = $server;
};
// passthrough hack
// all nested coroutines are allowed to do their own filtering
$coroutines[] = (new FetchingCoroutine($fetch_job()))
->set_response_filter(function(CurlHandle $handle) {
return true;
});
}
$runner = new FetchingCoroutineRunner($coroutines);
$runner->run_all();
return $reachable_servers;
}
/**
* Returns the URL scheme of this server.
* @return string "http" or "https".
*/
function get_scheme() {
return parse_url($this->base_url, PHP_URL_SCHEME);
}
/**
* Reduces this server's base URL to HTTP.
*/
function downgrade_scheme() {
$base_url = $this->base_url;
$this->base_url = "http://" . $this->get_hostname();
log_info("Downgrading $base_url to HTTP.");
}
/**
* Returns the hostname for this server.
* @return string URL with hostname and port, if applicable.
* Scheme not included.
*/
function get_hostname() {
return url_get_base($this->base_url, include_scheme: false);
}
/**
* Returns the server's root URL.
* @return string URL with scheme, hostname, and port, if applicable.
*/
function get_base_url() {
return $this->base_url;
}
/**
* Returns the URL to the endpoint listing this server's rooms.
*/
function get_rooms_api_url(): string {
$base_url = $this->base_url;
return "$base_url/rooms?all=1";
}
/**
* Returns the URL for the endpoint of the particular room.
*/
function get_room_api_url(string $token): string {
$base_url = $this->base_url;
return "$base_url/room/$token";
}
/**
* Returns the server's public key.
* @return string SOGS pubkey as used in the Session protocol.
*/
function get_pubkey() {
return $this->pubkey;
}
/**
* Attempts to set the server public key.
* @param string $pubkey SOGS public key.
* @return bool True if successful, false in case of mismatch.
*/
function set_pubkey(string $pubkey): bool {
if ($this->has_pubkey() && $this->pubkey != $pubkey) {
return false;
}
$this->pubkey = $pubkey;
return true;
}
/**
* Attempts to read the server public key from a join URL.
* @param string $join_url Join URL for any of the server's rooms.
* @return bool True if successful, false in case of mismatch.
*/
function set_pubkey_from_url(string $join_url): bool {
return $this->set_pubkey(url_get_pubkey($join_url));
}
/**
* Learns server info from a room's join URL.
* The base URL and public key are saved,
* and the room token is added as a fallback for room polling.
* @param string $join_url Room join URL to initialize with.
* @return bool True if successful, false in case of public key mismatch.
*/
function initialize_from_url($join_url): bool {
if (!$this->set_pubkey_from_url($join_url)) {
return false;
}
$this->base_url = url_get_base($join_url);
$this->room_hints[] = url_get_token($join_url);
return true;
}
/**
* Checks whether the current server SOGS public key is initialized.
* @return bool False if the public key is empty, true otherwise.
*/
function has_pubkey(): bool {
return $this->pubkey != "";
}
/**
* Returns the room of the given token, or null if one does not exist.
*/
function get_room_by_token(string $token): \CommunityRoom | null {
$candidates = array_filter($this->rooms, function(\CommunityRoom $room) use ($token) {
return $room->token == $token;
});
/** Filter doesn't reindex */
foreach ($candidates as $candidate) {
return $candidate;
}
return null;
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,array|null>
*/
private function fetch_room_list_coroutine(): Generator {
global $FAST_FETCH_MODE;
$base_url = $this->base_url;
/** @var CurlHandle|false $rooms_api_response */
$rooms_api_response =
yield from FetchingCoroutine
::from_url($this->get_rooms_api_url())
->retryable($FAST_FETCH_MODE ? 2 : 4)
->downgradeable($did_downgrade)
->run();
$rooms_raw = $rooms_api_response ? curl_multi_getcontent($rooms_api_response) : null;
if (!$rooms_raw) {
log_info("Failed fetching /rooms for $base_url.");
return null;
}
if ($did_downgrade) $this->downgrade_scheme();
$room_data = json_decode($rooms_raw, true);
if ($room_data == null) {
log_info("Failed parsing /rooms for $base_url.");
return null;
}
log_debug("Fetched /rooms successfully for $base_url");
// log_value($room_data);
return $room_data;
}
/**
* @return Generator<int,CurlHandle,CurlHandle|false,array|null>
*/
private function fetch_room_hints_coroutine(): Generator {
global $FAST_FETCH_MODE;
$base_url = $this->base_url;
$rooms = [];
if (empty($this->room_hints)) {
log_debug("No room hints to scan for $base_url.");
return null;
}
foreach ($this->room_hints as $token) {
log_debug("Testing room /$token at $base_url.");
// Note: This fetches room hints sequentially per each server
// Would need to allow yielding handle arrays
// More than good enough for now
$room_api_response = yield from FetchingCoroutine
::from_url($this->get_room_api_url($token))
// Afford more attempts thanks to reachability test
// TODO Move retryability to outer invocation
->retryable(retries: $FAST_FETCH_MODE ? 2 : 4)
->downgradeable($did_downgrade)
->run();
$room_raw = $room_api_response ? curl_multi_getcontent($room_api_response) : null;
if (!$room_raw) {
log_info("Room /$token not reachable at $base_url.");
continue;
}
if ($did_downgrade) $this->downgrade_scheme();
$room_data = json_decode($room_raw, true);
if ($room_data == null) {
if (count($rooms) == 0) {
log_info("Room /$token not parsable at $base_url.");
break;
} else {
log_debug("Room /$token not parsable at $base_url, continuing.");
continue;
}
}
$rooms[] = $room_data;
}
// Mark no rooms as failure.
if (empty($rooms)) {
log_debug("No room hints were valid at $base_url.");
return null;
}
return $rooms;
}
function check_reachability_coroutine() {
global $FAST_FETCH_MODE;
$base_url = $this->base_url;
log_info("Checking reachability for $base_url first...");
/** @var CurlHandle|false $response_handle */
$response_handle =
yield from FetchingCoroutine
::from_url($base_url, [CURLOPT_NOBODY => true])
->set_response_filter(function (CurlHandle $handle) {
$code = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
log_debug("Got $code for $url in custom filter.");
return $code != 0;
})
->retryable(retries: $FAST_FETCH_MODE ? 2 : 4)
->downgradeable($did_downgrade)
->run();
if (!$response_handle) {
log_warning("Reachability test failed by $base_url.");
return false;
}
if ($did_downgrade) $this->downgrade_scheme();
return true;
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,bool>
*/
function fetch_rooms_coroutine(): Generator {
$this->log_details();
$base_url = $this->base_url;
// Check reachability before polling too much.
if (count($this->room_hints) >= 2) {
if (!yield from $this->check_reachability_coroutine()) {
return false;
}
}
log_info("Fetching rooms for $base_url.");
/** @var array|null $room_data */
$room_data =
(yield from $this->fetch_room_list_coroutine()) ??
(yield from $this->fetch_room_hints_coroutine());
if ($room_data === null) {
log_warning("Could not fetch rooms for $base_url.");
return false;
}
$this->rooms = CommunityRoom::from_details_array($this, $room_data);
return true;
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,bool>
*/
function fetch_pubkey_coroutine(): Generator {
global $FAST_FETCH_MODE;
$base_url = $this->base_url;
if (empty($this->rooms)) {
log_warning("Server $base_url has no rooms to poll for public key");
return false;
}
$has_pubkey = $this->has_pubkey();
if ($has_pubkey && $FAST_FETCH_MODE) {
return true;
}
$preview_url = $this->rooms[0]->get_preview_url();
log_info("Fetching pubkey from $preview_url");
$room_view_response = yield from FetchingCoroutine
::from_url($preview_url)
->retryable($has_pubkey || $FAST_FETCH_MODE ? 1 : 5)
->run();
$room_view = $room_view_response
? curl_multi_getcontent($room_view_response)
: null;
if (!$room_view) {
log_debug("Failed to fetch room preview from $preview_url.");
return $has_pubkey;
}
$links = parse_join_links($room_view);
$link = $links[0];
if (!isset($link)) {
log_debug("Could not locate join link in preview at $preview_url.");
return $has_pubkey;
}
if (!$this->set_pubkey_from_url($link)) {
// More information needs to be logged for errors
// in case of lack of context due to lower verbosity.
$base_url = $this->base_url;
$pubkey_old = $this->pubkey;
$pubkey_new = url_get_pubkey($link);
log_error(
"Key collision for $base_url:" .
"Have $pubkey_old, fetched $pubkey_new from $preview_url"
);
return false;
}
return true;
}
/**
* @param \CommunityServer $servers
*/
public static function fetch_assets(array $servers) {
// Sequential in each server, see note in fetch_room_hints_coroutine()
$coroutines = [];
foreach (CommunityServer::enumerate_rooms($servers) as $room) {
$coroutines[] = new FetchingCoroutine((function() use ($room) {
yield from fetch_qr_code_coroutine($room);
yield from fetch_room_icon_coroutine($room);
})());
}
(new FetchingCoroutineRunner($coroutines))->run_all();
}
/**
* Checks whether this server belongs to Session / OPTF.
*/
function is_official_server() {
global $KNOWN_PUBKEYS;
return (
$this->base_url == "https://open.getsession.org" &&
$this->pubkey == $KNOWN_PUBKEYS['open.getsession.org']
);
}
}
?>