|
|
|
@ -61,16 +61,6 @@
|
|
|
|
|
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.
|
|
|
|
@ -80,8 +70,6 @@
|
|
|
|
|
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'];
|
|
|
|
@ -98,18 +86,26 @@
|
|
|
|
|
$this->write = $details['write'];
|
|
|
|
|
$this->upload = $details['upload'];
|
|
|
|
|
|
|
|
|
|
$this->extract_tags_from_description();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const DESCRIPTION_TAGS_SPECIFICATION = '/(#[^#()@., ]+(?:,?\s*|\s+|$))+\s*$/';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return an optional Unicode emoji of region matching
|
|
|
|
|
* the primary language of this room.
|
|
|
|
|
*/
|
|
|
|
|
public function get_language_flag(): string {
|
|
|
|
|
global $languages;
|
|
|
|
|
|
|
|
|
|
$room_identifier = $this->get_room_identifier();
|
|
|
|
|
|
|
|
|
|
$this->language_flag =
|
|
|
|
|
isset($languages[$room_identifier])
|
|
|
|
|
return isset($languages[$room_identifier])
|
|
|
|
|
? $languages[$room_identifier]
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
$this->extract_tags_from_description();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const DESCRIPTION_TAGS_SPECIFICATION = '/(#[^#()@., ]+(?:,?\s*|\s+|$))+\s*$/';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pre-processes SOGS data by treating description-trailing hashtags as room tags.
|
|
|
|
|
*/
|
|
|
|
@ -151,6 +147,7 @@
|
|
|
|
|
unset($details['server']);
|
|
|
|
|
$details['tags'] = $this->get_raw_tags();
|
|
|
|
|
$details['tags_custom'] = $this->get_derived_tags();
|
|
|
|
|
$details['language_flag'] = $this->get_language_flag();
|
|
|
|
|
return $details;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -166,7 +163,7 @@
|
|
|
|
|
"room" => $details,
|
|
|
|
|
"room_extra" => array(
|
|
|
|
|
"join_url" => $this->get_join_url(),
|
|
|
|
|
"language_flag" => $this->language_flag,
|
|
|
|
|
"language_flag" => $this->get_language_flag(),
|
|
|
|
|
"tags" => $this->get_raw_tags()
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
@ -288,7 +285,7 @@
|
|
|
|
|
*/
|
|
|
|
|
function get_join_url(): string {
|
|
|
|
|
$base_url = $this->server->base_url;
|
|
|
|
|
$pubkey = $this->server->pubkey;
|
|
|
|
|
$pubkey = $this->server->get_pubkey();
|
|
|
|
|
$token = $this->token;
|
|
|
|
|
return "$base_url/$token?public_key=$pubkey";
|
|
|
|
|
}
|
|
|
|
@ -314,7 +311,7 @@
|
|
|
|
|
*/
|
|
|
|
|
function get_room_identifier(): string {
|
|
|
|
|
$token = $this->token;
|
|
|
|
|
$pubkey_4 = substr($this->server->pubkey, 0, 4);
|
|
|
|
|
$pubkey_4 = substr($this->server->get_pubkey(), 0, 4);
|
|
|
|
|
return "$token+$pubkey_4";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -513,14 +510,26 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum CommunityServerMergeStrategy {
|
|
|
|
|
case SameHostname;
|
|
|
|
|
case SamePublicKey;
|
|
|
|
|
|
|
|
|
|
public function should_merge_servers(\CommunityServer $a, \CommunityServer $b) {
|
|
|
|
|
return match ($this) {
|
|
|
|
|
CommunityServerMergeStrategy::SameHostname => $a->get_hostname() == $b->get_hostname(),
|
|
|
|
|
CommunityServerMergeStrategy::SamePublicKey => $a->get_pubkey() == $b->get_pubkey()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 string[] $pubkey_candidates Possible SOGS protocol pubkeys for this server. */
|
|
|
|
|
private array $pubkey_candidates = [];
|
|
|
|
|
/** @var ?\CommunityRoom[] Array of rooms hosted by this server. */
|
|
|
|
|
public ?array $rooms = null;
|
|
|
|
|
/**
|
|
|
|
@ -571,7 +580,7 @@
|
|
|
|
|
* respectively.
|
|
|
|
|
*/
|
|
|
|
|
static function compare_by_pubkey($a, $b): int {
|
|
|
|
|
return strcmp($a->pubkey, $b->pubkey);
|
|
|
|
|
return strcmp($a->get_pubkey(), $b->get_pubkey());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -579,41 +588,48 @@
|
|
|
|
|
* @param CommunityServer[] $servers
|
|
|
|
|
*/
|
|
|
|
|
public static function sort_by_pubkey(&$servers) {
|
|
|
|
|
foreach ($servers as $server) {
|
|
|
|
|
if (count($server->pubkey_candidates) != 1) {
|
|
|
|
|
$server->log_details();
|
|
|
|
|
$base_url = $server->base_url;
|
|
|
|
|
log_error("Server $base_url does not have a resolved pubkey before pubkey de-duping.");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
usort($servers, 'CommunityServer::compare_by_pubkey');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function absorb_pubkeys_from($server): void {
|
|
|
|
|
$this->pubkey_candidates = [
|
|
|
|
|
...$this->pubkey_candidates,
|
|
|
|
|
...$server->pubkey_candidates
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Absorbs extra info from another instance of the same server.
|
|
|
|
|
* @param CommunityServer $server
|
|
|
|
|
*
|
|
|
|
|
* @return True if successful, false in case of mismatch.
|
|
|
|
|
*/
|
|
|
|
|
private function merge_from($server): bool {
|
|
|
|
|
private function merge_from($server, \CommunityServerMergeStrategy $strategy): 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;
|
|
|
|
|
if ($strategy == CommunityServerMergeStrategy::SameHostname) {
|
|
|
|
|
if ($this->get_hostname() != $server->get_hostname()) {
|
|
|
|
|
log_error("SameHostname merging: Merged servers differ in hostname");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
$this->absorb_pubkeys_from($server);
|
|
|
|
|
} else if ($strategy == CommunityServerMergeStrategy::SamePublicKey) {
|
|
|
|
|
if ($this->get_pubkey() != $server->get_pubkey()) {
|
|
|
|
|
log_error("SamePublicKey merging: Merged servers differ in public key");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
$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.
|
|
|
|
@ -643,6 +659,7 @@
|
|
|
|
|
// Remove duplicate room hints; does not require sorting.
|
|
|
|
|
foreach ($servers as $server) {
|
|
|
|
|
$server->room_hints = array_unique($server->room_hints);
|
|
|
|
|
$server->pubkey_candidates = array_unique($server->pubkey_candidates);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $servers;
|
|
|
|
@ -653,13 +670,13 @@
|
|
|
|
|
* @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) {
|
|
|
|
|
private static function merge_by(&$servers, \CommunityServerMergeStrategy $strategy) {
|
|
|
|
|
// 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()) {
|
|
|
|
|
if ($strategy->should_merge_servers($servers[$i], $servers[$i - 1])) {
|
|
|
|
|
// Merge this server into the previous one, discarding it.
|
|
|
|
|
$servers[$i - 1]->merge_from($servers[$i]);
|
|
|
|
|
$servers[$i - 1]->merge_from($servers[$i], $strategy);
|
|
|
|
|
array_splice($servers, $i, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -672,7 +689,7 @@
|
|
|
|
|
$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";
|
|
|
|
|
$pubkey = $this->has_pubkey() ? truncate($this->get_pubkey(), 4) : "unknown";
|
|
|
|
|
log_debug("Server $base_url"."[$count_rooms/$count_room_hints] { pubkey: $pubkey }");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -684,7 +701,7 @@
|
|
|
|
|
public static function dedupe_by_url($servers) {
|
|
|
|
|
CommunityServer::sort_by_url($servers);
|
|
|
|
|
|
|
|
|
|
CommunityServer::merge_by($servers, "get_hostname");
|
|
|
|
|
CommunityServer::merge_by($servers, CommunityServerMergeStrategy::SameHostname);
|
|
|
|
|
|
|
|
|
|
$servers = CommunityServer::ensure_merge_consistency($servers);
|
|
|
|
|
|
|
|
|
@ -699,7 +716,7 @@
|
|
|
|
|
public static function dedupe_by_pubkey($servers) {
|
|
|
|
|
CommunityServer::sort_by_pubkey($servers);
|
|
|
|
|
|
|
|
|
|
CommunityServer::merge_by($servers, "get_pubkey");
|
|
|
|
|
CommunityServer::merge_by($servers, CommunityServerMergeStrategy::SamePublicKey);
|
|
|
|
|
|
|
|
|
|
$servers = CommunityServer::ensure_merge_consistency($servers);
|
|
|
|
|
|
|
|
|
@ -713,6 +730,8 @@
|
|
|
|
|
$details = get_object_vars($this);
|
|
|
|
|
unset($details['room_hints']);
|
|
|
|
|
unset($details['merge_error']);
|
|
|
|
|
unset($details['pubkey_candidates']);
|
|
|
|
|
$details['pubkey'] = $this->get_pubkey();
|
|
|
|
|
return $details;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -738,7 +757,7 @@
|
|
|
|
|
throw new Error("Known server $hostname has no known pubkey");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$server->pubkey = $pubkeys[$hostname];
|
|
|
|
|
$server->set_pubkey($pubkeys[$hostname]);
|
|
|
|
|
|
|
|
|
|
$servers[] = $server;
|
|
|
|
|
}
|
|
|
|
@ -776,7 +795,7 @@
|
|
|
|
|
$server = new CommunityServer();
|
|
|
|
|
|
|
|
|
|
$server->base_url = $details['base_url'];
|
|
|
|
|
$server->pubkey = $details['pubkey'];
|
|
|
|
|
$server->set_pubkey($details['pubkey']);
|
|
|
|
|
$server->rooms = CommunityRoom::from_details_array($server, $details['rooms']);
|
|
|
|
|
|
|
|
|
|
return $server;
|
|
|
|
@ -911,7 +930,13 @@
|
|
|
|
|
* @return string SOGS pubkey as used in the Session protocol.
|
|
|
|
|
*/
|
|
|
|
|
function get_pubkey() {
|
|
|
|
|
return $this->pubkey;
|
|
|
|
|
if (!$this->has_pubkey()) {
|
|
|
|
|
$base_url = $this->base_url;
|
|
|
|
|
$count = count($this->pubkey_candidates);
|
|
|
|
|
log_error("Cannot get pubkey of server $base_url: has $count");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
return $this->pubkey_candidates[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -920,11 +945,11 @@
|
|
|
|
|
* @return bool True if successful, false in case of mismatch.
|
|
|
|
|
*/
|
|
|
|
|
function set_pubkey(string $pubkey): bool {
|
|
|
|
|
if ($this->has_pubkey() && $this->pubkey != $pubkey) {
|
|
|
|
|
if ($this->has_pubkey() && !in_array($pubkey, $this->pubkey_candidates, true)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->pubkey = $pubkey;
|
|
|
|
|
$this->pubkey_candidates = [$pubkey];
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
@ -956,10 +981,10 @@
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks whether the current server SOGS public key is initialized.
|
|
|
|
|
* @return bool False if the public key is empty, true otherwise.
|
|
|
|
|
* @return bool False if the public key is unknown, true otherwise.
|
|
|
|
|
*/
|
|
|
|
|
function has_pubkey(): bool {
|
|
|
|
|
return $this->pubkey != "";
|
|
|
|
|
private function has_pubkey(): bool {
|
|
|
|
|
return count($this->pubkey_candidates) == 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -1187,7 +1212,7 @@
|
|
|
|
|
// 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_old = $this->get_pubkey();
|
|
|
|
|
$pubkey_new = url_get_pubkey($link);
|
|
|
|
|
log_error(
|
|
|
|
|
"Key collision for $base_url:" .
|
|
|
|
@ -1223,7 +1248,7 @@
|
|
|
|
|
global $KNOWN_PUBKEYS;
|
|
|
|
|
return (
|
|
|
|
|
$this->base_url == "https://open.getsession.org" &&
|
|
|
|
|
$this->pubkey == $KNOWN_PUBKEYS['open.getsession.org']
|
|
|
|
|
$this->get_pubkey() == $KNOWN_PUBKEYS['open.getsession.org']
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|