|
|
|
<?php
|
|
|
|
|
|
|
|
include_once "$PROJECT_ROOT/languages/language_flags.php";
|
|
|
|
|
|
|
|
class CommunityRoom implements JsonSerializable {
|
|
|
|
public readonly object $server;
|
|
|
|
public readonly int $active_users;
|
|
|
|
public readonly int $active_users_cutoff;
|
|
|
|
public readonly string $token;
|
|
|
|
public readonly string $name;
|
|
|
|
public readonly array $admins;
|
|
|
|
public readonly array $moderators;
|
|
|
|
public readonly float $created;
|
|
|
|
public readonly string $description;
|
|
|
|
public readonly ?int $image_id;
|
|
|
|
public readonly int $info_updates;
|
|
|
|
public readonly int $message_sequence;
|
|
|
|
public readonly bool $read;
|
|
|
|
public readonly bool $upload;
|
|
|
|
public readonly bool $write;
|
|
|
|
|
|
|
|
// Custom properties
|
|
|
|
public readonly string $language_flag;
|
|
|
|
|
|
|
|
private function __construct($server, array $data) {
|
|
|
|
global $languages;
|
|
|
|
|
|
|
|
$this->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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
?>
|