Docs and minor refactoring

dev
gravel 2 years ago
parent 974441c6d0
commit 00bdd3a615
Signed by: gravel
GPG Key ID: C0538F3C906B308F

6
.gitignore vendored

@ -27,3 +27,9 @@ log.txt
# Kate
*.kate-swp
# Custom Doxygen configuration
Doxyfile
# Doxygen documentation
.doxygen

@ -1,28 +1,147 @@
<?php
/**
* \file
* PHP environment variables.
*/
/**
* @var string $PROJECT_ROOT
* Root directory of the project.
*/
$PROJECT_ROOT=__DIR__;
/**
* @var string $PHP_ROOT
* Root directory for PHP scripts.
*/
$PHP_ROOT="$PROJECT_ROOT/php";
/**
* @var string $CACHE_ROOT
* Root directory for temporary storage.
*/
$CACHE_ROOT="$PROJECT_ROOT/cache";
/**
* @var string $DOCUMENT_ROOT
* Root directory for main site documents.
*/
$DOCUMENT_ROOT="$PROJECT_ROOT/output";
/**
* @var string $ROOMS_FILE
* Path to file containing fetched servers.
*/
$ROOMS_FILE="$DOCUMENT_ROOT/servers.json";
/**
* @var string $TEMPLATES_ROOT
* Root directory containing sites in PHP.
*/
$TEMPLATES_ROOT="$PROJECT_ROOT/sites";
/**
* @var string $LANGUAGES_ROOT
* Directory containing languages module.
*/
$LANGUAGES_ROOT="$PROJECT_ROOT/languages";
/**
* @var string $QR_CODES
* Directory containing served QR codes.
*/
$QR_CODES="$DOCUMENT_ROOT/qr-codes";
/**
* @var string $QR_CODES_RELATIVE
* Web-relative path to served QR codes.
*/
$QR_CODES_RELATIVE="qr-codes";
/**
* @var string $ROOM_ICONS_CACHE
* Directory containing cached room icons.
*/
$ROOM_ICONS_CACHE="$CACHE_ROOT/icons";
/**
* @var string $ROOM_ICONS
* Directory containing served room icons.
*/
$ROOM_ICONS="$DOCUMENT_ROOT/icons";
/**
* @var string $ROOM_ICONS_RELATIVE
* Web-relative path to served room icons.
*/
$ROOM_ICONS_RELATIVE="icons";
/**
* @var string $LONG_TERM_CACHE_ROOT
* Root directory for long-term cached resources.
*/
$LONG_TERM_CACHE_ROOT="$PROJECT_ROOT/cache-lt";
/**
* @var string $SOURCES_CACHE
* Directory containing cached responses from Community sources.
*/
$SOURCES_CACHE="$LONG_TERM_CACHE_ROOT/sources";
/**
* @var string $LISTING_PROVIDER_ROOT
* Root directory for listing provider API resources.
*/
$LISTING_PROVIDER_ROOT="$PROJECT_ROOT/listings";
/**
* @var string $LISTINGS_INI
* Path to file containing Community listing configuration.
*/
$LISTINGS_INI="$LISTING_PROVIDER_ROOT/listings.ini";
/**
* @var string $LISTING_PROVIDER_OUTPUT
* Directory with content served by listing provider API.
*/
$LISTING_PROVIDER_OUTPUT="$LISTING_PROVIDER_ROOT/lp-output";
/**
* @var string $LISTING_PROVIDER_LISTING_SUMMARY
* File containing overview of served Community listings.
*/
$LISTING_PROVIDER_LISTING_SUMMARY="$LISTING_PROVIDER_OUTPUT/listings";
/**
* @var string $LISTING_PROVIDER_LISTINGS
* Directory of individual Community listings.
*/
$LISTING_PROVIDER_LISTINGS="$LISTING_PROVIDER_OUTPUT/listing";
/**
* @var string $REPOSITORY_CANONICAL_URL
* The canonical URL for this project's Git repository.
*/
$REPOSITORY_CANONICAL_URL="https://codeberg.org/gravel/sessioncommunities.online";
/**
* @var string $REPOSITORY_MIRROR_URL
* The mirror URL for this project's Git repository.
*/
$REPOSITORY_MIRROR_URL="https://lokilocker.com/gravel/sessioncommunities.online";
/**
* @var string $REPOSITORY_CANONICAL_URL_FILES
* The base URL for this project's repository files.
*/
$REPOSITORY_CANONICAL_URL_FILES="$REPOSITORY_CANONICAL_URL/src/branch/main";
/**
* @var string $API_CANONICAL_URL
* The base URL for the listing provider API.
*/
$API_CANONICAL_URL="https://lp.sessioncommunities.online/";
set_include_path(implode(PATH_SEPARATOR, array(

@ -55,7 +55,7 @@ Recommended:
**Identifier casing**: `snake_case` and `CONSTANT_CASE`
**Comments and documentation**: [PHPDoc](https://en.wikipedia.org/wiki/PHPDoc)
**Comments and documentation**: [Doxygen](https://en.wikipedia.org/wiki/Doxygen)
**Whitespace**:

File diff suppressed because it is too large Load Diff

@ -61,10 +61,15 @@ lan-server:
open:
nohup xdg-open "http://localhost:$(PORT)" >/dev/null 2>/dev/null
# Update Doxygen documentation on file change.
docs:
$(MAKE) WATCHCMD="doxygen -q" watchdog
# Update HTML on file change.
watchdog: WATCHCMD = $(MAKE) html
watchdog:
set -o pipefail; \
while :; do find . | grep -v ".git" | entr -nds "$(MAKE) html" && exit; done
while :; do find . | grep -v ".git" | entr -nds "$(WATCHCMD)" && exit; done
# Remove artefacts
clean:

@ -1,4 +1,9 @@
<?php
/**
* \file
* Implement the fetching of Community room icons.
*/
require_once 'servers/known-servers.php';
/**
@ -21,7 +26,7 @@
}
/**
* Return server path to room icon.
* Return web-facing path to room icon.
* @param string $room_id Id of room to locate icon for.
* @param string $size Image dimensions.
*/
@ -31,9 +36,11 @@
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,void>
* Fetch the icon of a Community and yield required network requests.
* @param CommunityRoom $room Community to fetch icon for.
* @return Generator<int,CurlHandle,CurlHandle|false,void>
*/
function fetch_room_icon_coroutine(\CommunityRoom $room): Generator {
function fetch_room_icon_coroutine(CommunityRoom $room): Generator {
$room_id = $room->get_room_identifier();
$icon_cached = room_icon_path($room_id);
$icon_expired = file_exists($icon_cached) && filemtime($icon_cached) < strtotime("-1 day");
@ -58,12 +65,12 @@
}
/**
* Fetch the icon of the given room and return its relative path.
* @param \CommunityRoom $room
* Resize a fetched icon of the given room and return its relative path.
* @param CommunityRoom $room
* @param string $size Image dimensions.
* @return string Relative path or null if icon is absent.
*/
function room_icon(\CommunityRoom $room, string $size): ?string {
function room_icon(CommunityRoom $room, string $size): ?string {
list($width, $height) = explode("x", $size);
$width = intval($width);
$height = intval($height);

@ -1,4 +1,9 @@
<?php
/**
* \file
* Implement the fetching of room invite QR codes.
*/
/**
* Return local path to room invite code.
* @param string $room_id Id of room to locate QR code for.
@ -19,9 +24,11 @@
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,void>
* Fetches the QR code for the given Community, yielding required network requests.
* @param CommunityRoom $room Community to fetch QR code for
* @return Generator<int,CurlHandle,CurlHandle|false,void>
*/
function fetch_qr_code_coroutine(\CommunityRoom $room): Generator {
function fetch_qr_code_coroutine(CommunityRoom $room): Generator {
$room_id = $room->get_room_identifier();
$png_cached = room_qr_code_path($room_id);
$image_expired = file_exists($png_cached) &&
@ -43,10 +50,10 @@
/**
* Fetch QR invite of the given room and return its relative path.
* @param \CommunityRoom $room
* @param CommunityRoom $room
* @return string
*/
function room_qr_code(\CommunityRoom $room): string {
function room_qr_code(CommunityRoom $room): string {
$room_id = $room->get_room_identifier();
if (!file_exists(room_qr_code_path($room_id))) {
log_warning("Missing QR code asset for $room_id.");

@ -1,14 +1,19 @@
<?php
/**
* \file
* Implement the fetching of Community server icons.
*/
require_once 'servers/known-servers.php';
require_once 'assets/room-icons.php';
/**
* Fetch the icon of the given Community server and return its relative path.
* @param \CommunityServer $server
* @param CommunityServer $server
* @param string $size Image dimensions.
* @return string Relative path or null if icon is absent.
*/
function server_icon(\CommunityServer $server, string $size): ?string {
function server_icon(CommunityServer $server, string $size): ?string {
global $SERVER_ICON_MAPPING;
$hostname = $server->get_hostname();
if (!isset($SERVER_ICON_MAPPING[$hostname])) {

@ -1,4 +1,9 @@
<?php
/**
* \file
* Fetch online Communities and write the resulting data to disk.
*/
// requires php-curl
require_once 'getenv.php';
@ -63,37 +68,6 @@
if (!$DO_DRY_RUN) file_put_contents($ROOMS_FILE, json_encode($servers));
}
/**
* Debug function to see which communities use pinned messages already
* @deprecated
*/
function print_pinned_messages($room_assignments_arr) {
// for each server a.k.a. public key do
foreach($room_assignments_arr as $pubkey => $room_assignment) {
// for every room do
foreach($room_assignment[1] as $room_array) {
// info:
// $room_array = array(
// "token" => bla,
// "name" => Blabla,
// "active_users" => -1,
// "description" => Blabla bla bla
//);
$server_url = $room_assignment[0];
$room_json_url = $server_url . "/room/" . $room_array["token"];
echo($room_json_url . PHP_EOL);
$contents = file_get_contents($room_json_url);
if($contents) {
// print_r($contents);
$json_obj = json_decode($contents);
$pinned_messages = $json_obj->pinned_messages;
echo("Pinned messages:" . PHP_EOL);
print_r($pinned_messages);
}
}
}
}
// Fetch servers
main();

@ -1,10 +1,16 @@
<?php
// Perform static site generation.
/**
* \file
* Perform static site generation.
*/
require_once 'getenv.php';
require_once 'utils/getopt.php';
// https://stackoverflow.com/a/17161106
/**
* Recursively match the last segment of the given path pattern.
* @source https://stackoverflow.com/a/17161106
*/
function rglob($pattern, $flags = 0) {
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
@ -16,6 +22,9 @@
return $files;
}
/**
* Generate HTML from PHP files in the templates directory.
*/
function generate_html() {
global $LOGGING_VERBOSITY, $TEMPLATES_ROOT, $DOCUMENT_ROOT;

@ -1,16 +1,44 @@
<?php
/**
* \file
* Implement Community listings for the [Listing Provider API](https://codeberg.org/gravel/session-listing-providers).
*/
require_once "getenv.php";
require_once "utils/logging.php";
require_once "servers/servers-rooms.php";
/**
* Represents a Community listing in the Listing Provider API.
*/
class CommunityListing implements JsonSerializable {
/**
* @var string $id
* Unique listing identifier.
*/
public readonly string $id;
/**
* @var string $name
* Human-readable listing name.
*/
public readonly string $name;
/**
* @var string $rating
* One-word content rating for Communities listed.
*/
public readonly string $rating;
/**
* @var CommunityRoom[] $rooms
* Communities included in the listing.
*/
public readonly array $rooms;
/**
* @param \CommunityRoom[] $rooms
* Create a new CommunityListing instance with the given parameters.
* @param string $id Unique listing identifier.
* @param string $name Human-readable listing name.
* @param string $rating One-word content rating for Communities listed.
* @param CommunityRoom[] $rooms Communities included in the listing.
*/
public function __construct(string $id, string $name, ?string $rating, array $rooms) {
$this->id = $id;
@ -19,15 +47,21 @@
$this->rooms = $rooms;
}
/**
* Produce associative listing data for JSON serialization.
*/
public function jsonSerialize(): mixed {
// TODO: Careful serialization
$details = get_object_vars($this);
$details['rooms'] = array_map(function(\CommunityRoom $room){
$details['rooms'] = array_map(function(CommunityRoom $room){
return $room->to_listing_data();
}, $this->rooms);
return $details;
}
/**
* Produce associative data summarizing this listing.
*/
public function to_summary(): array {
return array(
'id' => $this->id,
@ -39,23 +73,30 @@
}
/**
* @return \CommunityListing[]
* Construct Community listings from listing configuration and cached Communities.
* @return CommunityListing[]
* \todo Refactor
*/
function resolve_listings_config(): array {
global $LISTINGS_INI, $ROOMS_FILE;
$listings_raw = parse_ini_file($LISTINGS_INI, process_sections: true, scanner_mode: INI_SCANNER_RAW);
$servers_raw = file_get_contents($ROOMS_FILE);
$server_data = json_decode($servers_raw, true);
$servers = CommunityServer::from_details_array($server_data);
$rooms_all = CommunityServer::enumerate_rooms($servers);
$rooms_by_id = [];
foreach ($rooms_all as $room) {
$rooms_by_id[$room->get_room_identifier()] = $room;
}
$sogs_by_pubkey = [];
foreach ($servers as $server) {
$sogs_by_pubkey[$server->get_pubkey()] = $server;
}
$listings = [];
foreach ($listings_raw as $id => $listing_props) {
$rooms = [];
@ -76,7 +117,7 @@
if (isset($listing_props['sogs'])) {
foreach ($listing_props['sogs'] as $public_key) {
if (isset($sogs_by_pubkey[$public_key])) {
/** @var \CommunityServer $sogs */
/** @var CommunityServer $sogs */
$sogs = $sogs_by_pubkey[$public_key];
array_push($rooms, ...$sogs->rooms);
} else {
@ -86,7 +127,7 @@
}
$rooms = array_filter($rooms, function(CommunityRoom $room) {
return !$room->room_is_off_record();
return !$room->is_off_record();
});
$listings[] = new CommunityListing(
@ -100,12 +141,15 @@
return $listings;
}
/**
* Resolve and write configured Community listings to disk.
*/
function generate_listings() {
global $LISTING_PROVIDER_LISTING_SUMMARY, $LISTING_PROVIDER_LISTINGS;
log_info("Generating listings...");
$listings_resolved = resolve_listings_config();
$summaries = array_map(function(\CommunityListing $listing) {
$summaries = array_map(function(CommunityListing $listing) {
return $listing->to_summary();
}, $listings_resolved);
file_put_contents($LISTING_PROVIDER_LISTING_SUMMARY, json_encode($summaries));

@ -1,4 +1,13 @@
<?php
/**
* \file
* Source the project's `.phpenv` file.
*/
/**
* @var $PROJECT_ROOT
* Root directory of the project.
*/
$PROJECT_ROOT = dirname(__FILE__);
(function(){

@ -1,6 +1,12 @@
<?php
/**
* List of servers hosts queried individually.
* \file
* Provide hardcoded information on Community servers and Communities.
*/
/**
* @var $KNOWN_SERVERS
* Hardcoded Community server URLs.
*/
$KNOWN_SERVERS = array(
// Official server
@ -40,7 +46,11 @@
// [redacted]
);
// Alphabetically sorted keys.
/**
* @var $KNOWN_PUBKEYS
* Associative array of SOGS public keys by server hostname.
* Keys are sorted alphabetically.
*/
$KNOWN_PUBKEYS = array(
"116.203.51.179" => "39016f991400c35a46e11e06cb2a64d6d8ab6652e484a556b14f7cf57ed7e73a",
"13.233.251.36:8081" => "efcaecf00aebf5b75e62cf1fd550c6052842e1415a9339406e256c8b27cd2039",
@ -61,7 +71,7 @@
/**
* @var string[] $ICON_ALLOWLIST
* These hostnames are considered to have safe room icons.
* Hostnames considered to have safe room icons.
*/
$ICON_ALLOWLIST = [
"open.getsession.org",
@ -71,7 +81,7 @@
/**
* @var string[] $ICON_BLOCKLIST
* These hostnames or rooms are considered to have unsafe room icons.
* Hostnames or rooms considered to have unsafe room icons.
*/
$ICON_BLOCKLIST = [
@ -79,7 +89,7 @@
/**
* @var string[] $NSFW_INCLUDE
* These hostnames or rooms are considered to host NSFW content.
* Hostnames or rooms considered to host adult content.
*/
$NSFW_INCLUDE = [
"gaohuangse.work",
@ -104,14 +114,15 @@
/**
* @var string[] $NSFW_EXCLUDE
* These hostnames or rooms are considered to host SFW-only content.
* Hostnames or rooms considered not to host adult content.
*/
$NSFW_EXCLUDE = [
"AISFW+fc30",
];
/**
* These rooms are intended for testing and not for a general userbase.
* @var string[] $TESTING_INCLUDE
* Rooms intended for testing and not for a general userbase.
*/
$TESTING_INCLUDE = [
"fishing+8e2e",
@ -121,11 +132,19 @@
"xyz+7908",
];
/**
* @var array<string,string> $SERVER_ICON_MAPPING
* Associative array specifies which room token provides a server-wide icon.
*/
$SERVER_ICON_MAPPING = [
"open.getsession.org" => "session",
"sog.caliban.org" => "privacy"
];
/**
* @var string[] $ROOMS_USED_BY_PROJECT
* Community rooms encouraged for use in discussing https://sessioncommmunities.online/.
*/
$ROOMS_USED_BY_PROJECT = [
"webdev+118d"
];

@ -1,60 +1,109 @@
<?php
/**
* \file
* Represent Session Communities and Session Open Group Servers.
*/
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;
require_once 'utils/numeric.php';
/**
* Representation of Session Community room.
*/
class CommunityRoom implements JsonSerializable {
/** @var CommunityServer $server Server this room belongs to. */
/**
* @var CommunityServer $server
* Session Open Group Server this room belongs to.
*/
public readonly object $server;
/** @var ?int $active_users Number of active users in the defined period. */
/**
* @var int|null $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. */
/**
* @var int|null $active_users_cutoff
* Period for `$active_users`, in seconds.
*/
public readonly ?int $active_users_cutoff;
/** @var string $token Room name in Community API. */
/**
* @var string $token
* Unique room identifier within server.
*/
public readonly string $token;
/** @var ?string $name User-facing name given to Community. */
/**
* @var string|null $name
* User-facing name of Community.
*/
public readonly ?string $name;
/** @var ?string[] $admins The mixed Session IDs of public room admins. */
/**
* @var string[]|null $admins
* The mixed Session IDs of public room admins.
*/
public readonly ?array $admins;
/** @var ?string[] $moderators The mixed Session IDs of public room moderators. */
/**
* @var string[]|null $moderators
* The mixed Session IDs of public room moderators.
*/
public readonly ?array $moderators;
/** @var ?float $created UNIX timestamp of room creation, in seconds. */
/**
* @var float|null $created
* UNIX timestamp of room creation, in seconds.
*/
public readonly ?float $created;
/** @var ?string $description User-facing description given to Community. */
/**
* @var string|null $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. */
/**
* @var int|null $image_id
* File number for this room's icon; optional.
*/
public readonly ?int $image_id;
/** @var ?int $info_updates Monotonic integer counter that increases
* whenever the room's metadata changes. */
/**
* @var int|null $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. */
/**
* @var int|null $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
* @var bool|null $read
* This boolean flag indicates whether a regular user
* has permission to read messages in this room.
*/
public readonly ?bool $read;
/**
* @var ?bool $upload
* @var bool|null $upload
* This boolean flag indicates whether a regular user
* has permission to upload files to this room.
*/
public readonly ?bool $upload;
/**
* @var ?bool $write
* @var bool|null $write
* This boolean flag indicates whether a regular user
* has permission to write messages to this room.
*/
@ -69,7 +118,7 @@
*/
private array $tags = [];
private function __construct(\CommunityServer $server, array $details) {
private function __construct(CommunityServer $server, array $details) {
$this->server = $server;
$this->active_users = $details['active_users'];
$this->active_users_cutoff = $details['active_users_cutoff'];
@ -89,6 +138,9 @@
$this->extract_tags_from_description();
}
/**
* Regular expression matching tags specified in the Community description.
*/
private const DESCRIPTION_TAGS_SPECIFICATION = '/(#[^#()@., ]+(?:,?\s*|\s+|$))+\s*$/';
@ -109,7 +161,7 @@
/**
* Pre-processes SOGS data by treating description-trailing hashtags as room tags.
*/
function extract_tags_from_description() {
private function extract_tags_from_description() {
$matches = [];
if (!preg_match(CommunityRoom::DESCRIPTION_TAGS_SPECIFICATION, $this->description, $matches)) {
return;
@ -135,14 +187,14 @@
/**
* Returns true if room should not be reflected in listings.
*/
function room_is_off_record(): bool {
public function is_off_record(): bool {
return !$this->read;
}
/**
* Return information for JSON serialization.
* Produce associative data for JSON serialization.
*/
function jsonSerialize(): array {
public function jsonSerialize(): array {
$details = get_object_vars($this);
unset($details['server']);
$details['tags'] = $this->get_raw_tags();
@ -152,9 +204,9 @@
}
/**
* Return information for JSON serialization in listing.
* Produce associative data for JSON serialization to Community listings.
*/
function to_listing_data(): array {
public function to_listing_data(): array {
$details = get_object_vars($this);
unset($details['server']);
unset($details['tags']);
@ -170,7 +222,7 @@
}
/**
* Create a CommunityRoom instance from loaded data.
* Create a CommunityRoom instance from associative data.
* @param CommunityServer $server
*/
public static function from_details($server, array $details) {
@ -183,7 +235,7 @@
}
/**
* Create an array of CommunityRoom instances from loaded data.
* Create an array of CommunityRoom instances from associative data.
* @param array[] $details
* @return CommunityRoom[]
*/
@ -194,12 +246,12 @@
}
/**
* Sorts Community rooms in-place by the given string property.
* @param \CommunityRoom[] $rooms Rooms to sort by given key.
* Sort 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) {
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) use ($key) {
return strcmp(
$a->$key,
$b->$key
@ -208,11 +260,11 @@
}
/**
* Sorts Community rooms in-place by their server's public key.
* @param \CommunityRoom[] $rooms Rooms to sort by server pubkey.
* Sort 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) {
usort($rooms, function(CommunityRoom $a, CommunityRoom $b) {
return strcmp(
$a->server->get_pubkey(),
$b->server->get_pubkey()
@ -221,45 +273,28 @@
}
/**
* Returns array of staff Session IDs.
* Return all known Community staff Session IDs.
* @return string[]
*/
function get_staff() {
function get_staff(): array {
return array_unique(
[...$this->admins, ...$this->moderators]
);
}
/**
* Returns seconds elapsed since room was created.
* Return duration in seconds 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.
* Formats the active user cutoff period as a duration string.
* @return string|null Period over which active users are counted in huamn-readable form.
*/
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 format_duration($this->active_users_cutoff);
}
/**
@ -306,7 +341,7 @@
}
/**
* Return our format of room identifier.
* Return a globally unique room identifier.
* @return string String in the form `token+pubkey[:4]`.
*/
function get_room_identifier(): string {
@ -323,12 +358,16 @@
}
/**
* Add string tags to the Community.
* @param string[] $tags
*/
public function add_tags(array $tags) {
$this->tags = [...$this->tags, ...$tags];
}
/**
* Check whether the Community's text fields contain adult keywords.
*/
private function has_nsfw_keywords(): bool {
// Description not included due to false positives.
$blob =
@ -344,12 +383,21 @@
return false;
}
/**
* Check whether the given list matches the current Community or its parent server.
* @param string[] $filter
* Array of unique room identifiers, server pubkeys and/or server hostnames.
* @return bool True if the array matches the Community, false otherwise.
*/
public function matched_by_list(array $filter): bool {
return in_array($this->get_room_identifier(), $filter) ||
in_array($this->server->get_pubkey(), $filter) ||
in_array($this->server->get_hostname(), $filter);
}
/**
* Determine whether the Community is not safe for work.
*/
public function rated_nsfw(): bool {
global $NSFW_INCLUDE, $NSFW_EXCLUDE;
@ -360,6 +408,10 @@
return $this->has_nsfw_keywords() || $this->matched_by_list($NSFW_INCLUDE);
}
/**
* Determine the safety of the Community's icon.
* @return 1 if safe, -1 if unsafe, 0 if unknown.
*/
public function icon_safety(): int {
global $ICON_ALLOWLIST, $ICON_BLOCKLIST;
@ -373,16 +425,33 @@
return 0;
}
public const USERS_PER_STAFF = 50;
public const USERS_PER_STAFF_WARNING = 200;
public const MINIMUM_STAFF = 2;
/**
* Estimate for minimum number of users covered by one member of Community staff.
*/
private const USERS_PER_STAFF = 50;
/**
* Estimate for maximum number of users covered by one member of Community staff.
*/
private const USERS_PER_STAFF_WARNING = 200;
/**
* Number of minimum staff needed to moderate a Community.
*/
private const MINIMUM_STAFF = 2;
/**
* Estimate whether the Community has enough staff.
*/
private function has_good_staff_rating(): bool {
$recommended_staff_count = $this->active_users / CommunityRoom::USERS_PER_STAFF;
$staff_count = count($this->get_staff());
return $staff_count >= $recommended_staff_count && $staff_count >= CommunityRoom::MINIMUM_STAFF;
}
/**
* Estimate whether the Community does not have enough staff.
*/
private function has_poor_staff_rating(): bool {
if ($this->active_users <= 3) {
return false;
@ -392,20 +461,22 @@
}
/**
* @return string[]
* Return the string tags associated with this Community
* @return string[] Array of unique string tags.
*/
function get_raw_tags(): array {
private function get_raw_tags(): array {
return array_unique(array_values($this->tags));
}
/**
* Return the deroved tags associated with this room.
* Return the derived tags associated with this room.
* @return CommunityTag[] Array of tags.
*/
function get_derived_tags(): array {
private function get_derived_tags(): array {
global $ROOMS_USED_BY_PROJECT, $TESTING_INCLUDE;
/**
* @var \CommunityTag[] $derived_tags
* @var CommunityTag[] $derived_tags
*/
$derived_tags = [];
@ -502,7 +573,7 @@
/**
* Return the tags associated with this room.
* @return \CommunityTag[] Tags as string array.
* @return CommunityTag[] Array of tags.
*/
function get_room_tags(): array {
$user_tags = CommunityTag::from_user_tags($this->tags, remove_redundant: true);
@ -510,11 +581,29 @@
}
}
/**
* Specifies criteria used to merge data in CommunityServer instances.
*/
enum CommunityServerMergeStrategy {
/**
* @var SameHostname
* Strategy considering two servers to be identical if they share a hostname.
*/
case SameHostname;
/**
* @var SamePublicKey
* Strategy considering two servers to be identical if they share a SOGS public key.
*/
case SamePublicKey;
public function should_merge_servers(\CommunityServer $a, \CommunityServer $b) {
/**
* Determine whether two CommunityServer instances are identical under the given criteria.
* @param CommunityServer $a CommunityServer to compare.
* @param CommunityServer $b CommunityServer to compare.
* @return bool True if we know that the given CommunityServer instances refer to the same server.
*/
public function should_merge_servers(CommunityServer $a, CommunityServer $b): bool {
return match ($this) {
CommunityServerMergeStrategy::SameHostname => $a->get_hostname() == $b->get_hostname(),
CommunityServerMergeStrategy::SamePublicKey => $a->get_pubkey() == $b->get_pubkey()
@ -526,12 +615,24 @@
* Class representing Session Community server hosting Community rooms.
*/
class CommunityServer implements JsonSerializable {
/** @var string $base_url The root URL of this server. */
/**
* @var string $base_url
* The root URL of this server.
*/
public string $base_url = "";
/** @var string[] $pubkey_candidates Possible SOGS protocol pubkeys for this server. */
/**
* @var string[] $pubkey_candidates
* Possible SOGS protocol pubkeys for this server.
**/
private array $pubkey_candidates = [];
/** @var ?\CommunityRoom[] Array of rooms hosted by this server. */
/**
* @var CommunityRoom[]|null $rooms
* Array of Communities hosted by this server.
*/
public ?array $rooms = null;
/**
* @var string[] $room_hints
* This array contains fallback room tokens collected from links.
@ -599,7 +700,10 @@
usort($servers, 'CommunityServer::compare_by_pubkey');
}
private function absorb_pubkeys_from($server): void {
/**
* Absorb candidates for the SOGS public key from a duplicate server instance.
*/
private function merge_pubkeys_from(CommunityServer $server): void {
$this->pubkey_candidates = [
...$this->pubkey_candidates,
...$server->pubkey_candidates
@ -612,7 +716,7 @@
*
* @return True if successful, false in case of mismatch.
*/
private function merge_from($server, \CommunityServerMergeStrategy $strategy): bool {
private function merge_from($server, CommunityServerMergeStrategy $strategy): bool {
// Merge room hint information.
$this->room_hints = [
...$this->room_hints,
@ -624,7 +728,7 @@
log_error("SameHostname merging: Merged servers differ in hostname");
exit(1);
}
$this->absorb_pubkeys_from($server);
$this->merge_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");
@ -652,7 +756,7 @@
*/
private static function ensure_merge_consistency(array $servers) {
// Exclude servers with merge errors.
$servers = array_filter($servers, function(\CommunityServer $server) {
$servers = array_filter($servers, function(CommunityServer $server) {
return !$server->merge_error;
});
@ -670,7 +774,7 @@
* @param CommunityServer[] $servers Servers sorted by given attribute.
* @param string $method Method name to retrieve attribute from server.
*/
private static function merge_by(&$servers, \CommunityServerMergeStrategy $strategy) {
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--) {
@ -683,7 +787,7 @@
}
/**
* Write details about this server to debug log.
* Write details about this server to the debug log.
*/
private function log_details() {
$base_url = $this->base_url;
@ -694,9 +798,9 @@
}
/**
* Filters the given servers to remove URL duplicates.
* Filter the given servers to remove URL duplicates.
* @param CommunityServer[] $servers Servers to merge by URL.
* @return CommunityServer[] Merged URL-unique servers.
* @return CommunityServer[] Servers merged by URL.
*/
public static function dedupe_by_url($servers) {
CommunityServer::sort_by_url($servers);
@ -709,9 +813,10 @@
}
/**
* Filters the given servers to remove pubkey duplicates.
* Filter the given servers to remove pubkey duplicates.
* Servers must already have a determined public key.
* @param CommunityServer[] $servers Servers to merge by public key.
* @return CommunityServer[] Merged pubkey-unique servers.
* @return CommunityServer[] Servers merged by public key-
*/
public static function dedupe_by_pubkey($servers) {
CommunityServer::sort_by_pubkey($servers);
@ -818,10 +923,10 @@
/**
* Add to the given servers additional data extracted from our sources.
* @param \CommunityServer[] $servers
* @param \CommunitySources $source
* @param CommunityServer[] $servers
* @param CommunitySources $source
*/
static function source_additional_info(array $servers, \CommunitySources $source): void {
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());
@ -990,8 +1095,8 @@
/**
* 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) {
function get_room_by_token(string $token): CommunityRoom | null {
$candidates = array_filter($this->rooms, function(CommunityRoom $room) use ($token) {
return $room->token == $token;
});
@ -1004,7 +1109,8 @@
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,array|null>
* Fetch Community data from the server and yield required network requests.
* @return Generator<string,CurlHandle,CurlHandle|false,array|null>
*/
private function fetch_room_list_coroutine(): Generator {
global $FAST_FETCH_MODE;
@ -1042,6 +1148,7 @@
}
/**
* Fetch individual rooms and yield required network requests.
* @return Generator<int,CurlHandle,CurlHandle|false,array|null>
*/
private function fetch_room_hints_coroutine(): Generator {
@ -1104,6 +1211,10 @@
return $rooms;
}
/**
* Check whether the Community server is reachable and yield required requests.
* @return Generator<int,CurlHandle,CurlHandle|false,array|null>
*/
function check_reachability_coroutine() {
global $FAST_FETCH_MODE;
$base_url = $this->base_url;
@ -1135,7 +1246,8 @@
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,bool>
* Fetch Community data from public or observed information and yield required network requests.
* @return Generator<int,CurlHandle,CurlHandle|false,bool>
*/
function fetch_rooms_coroutine(): Generator {
$this->log_details();
@ -1165,7 +1277,8 @@
}
/**
* @return \Generator<int,CurlHandle,CurlHandle|false,bool>
* Fetch the Session Open Group Server public key and yield required network requests.
* @return Generator<int,CurlHandle,CurlHandle|false,bool>
*/
function fetch_pubkey_coroutine(): Generator {
global $FAST_FETCH_MODE;
@ -1225,7 +1338,7 @@
}
/**
* @param \CommunityServer $servers
* @param CommunityServer $servers
*/
public static function fetch_assets(array $servers) {
// Sequential in each server, see note in fetch_room_hints_coroutine()
@ -1242,7 +1355,7 @@
}
/**
* Checks whether this server belongs to Session / OPTF.
* Checks whether this server belongs to Session / Oxen Privacy Tech Foundation.
*/
function is_official_server() {
global $KNOWN_PUBKEYS;

@ -2,17 +2,30 @@
require_once 'utils/utils.php';
require_once 'servers/tags.php';
class SDIRCommunitySource {
/**
* Describes a type of source capable of producing a per-Community tag list.
*/
interface CommunitySourceWithTags {
/**
* 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;
}
/**
* Encapsulates Community data fetched from https://session.directory/.
*/
class SDIRCommunitySource implements CommunitySourceWithTags {
private function __construct(string $contents) {
$this->contents = $contents;
}
/**
* Create new instance of this source from contents.
* Create new instance of SDIRCommunitySource on the given source text.
* Returns false if processing the source fails.
* @return \SDIRCommunitySource|false
*/
public static function from_contents(string $contents) {
public static function from_contents(string $contents): SDIRCommunitySource | false {
$source = new SDIRCommunitySource($contents);
if (!$source->sdir_process_tags()) {
@ -99,6 +112,7 @@
}
/**
* 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 {
@ -106,7 +120,10 @@
}
}
class ASGLCommunitySource {
/**
* Encapsulates Community data 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;
}
@ -119,9 +136,10 @@
private array $tags = [];
/**
* @return \ASGLCommunitySource|false
* Attempt to create an ASGLCommunitySource instance on the given source text.
* Returns false if processing the source fails.
*/
public static function from_contents(string $contents) {
public static function from_contents(string $contents): ASGLCommunitySource | false {
$source = new ASGLCommunitySource($contents);
if(!$source->asgl_process_tags()) {
@ -182,12 +200,18 @@
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;
}
}
/**
* Encapsulates sources of Communities found on the web.
*/
class CommunitySources {
private const SOURCES = array(
'ASGL' => 'https://raw.githubusercontent.com/GNU-Linux-libre/Awesome-Session-Group-List/main/README.md',
@ -212,7 +236,7 @@
private array $room_tags = [];
/**
* Fetches and saves known sources of Session Community join links.
* Creates a new CommunitySources instance with processed Community data from the Web.
*/
public function __construct() {
log_info("Requesting Awesome Session Group list...");
@ -306,7 +330,7 @@
}
/**
* Returns all join URLs found.
* Return all known join links to Session Communities.
* @return string[] Join URLs.
*/
public function get_join_urls(): array {
@ -316,9 +340,9 @@
}
/**
* Return known tags for the given room.
* Return all known tags for the given room.
* @param string $room_id Room identifier.
* @return \CommunityTag[] Array of string tags.
* @return CommunityTag[] Array of string tags.
*/
public function get_room_tags($room_id): array {
if (!isset($this->room_tags[$room_id])) {

@ -1,18 +1,44 @@
<?php
/**
* \file
* Implement maintainer-given and derived tags for Community rooms.
*/
require_once 'utils/utils.php';
/**
* Class enumerating types of Community tags.
*/
class TagType {
private function __construct() {}
/**
* Specifies custom tag added by Community maintainer or Community source.
*/
const USER_TAG = 0;
/**
* Specifies basic type of tag reserved for assignment by our aggregator.
*/
const RESERVED_TAG = 1;
/**
* Specifies warning tag reserved for assignment by our aggregator.
*/
const WARNING_TAG = 2;
}
/**
* Represents a manual or derived Community tag.
*/
class CommunityTag implements JsonSerializable {
/**
* Create a new CommunityTag instance.
* @param string $text Text the tag should read.
* @param int $tag_type Numeric {@link TagType} value.
* @param string|null $description [optional] Brief explanation of tag.
*/
public function __construct(
string $text,
int $tag_type = TagType::USER_TAG,
string $description = ""
?string $description = ""
) {
$this->text = $text;
$this->type = $tag_type;
@ -20,26 +46,41 @@
empty($description) ? "Tag: $text" : $description;
}
/**
* @var int $type
* Tag type as given by a {@link TagType} value.
*/
public readonly int $type;
/**
* @var string $text
* The string tag itself.
*/
public readonly string $text;
/**
* @var string $description
* A text description of the tag.
*/
public readonly string $description;
/**
* Returns a lowercase representation of the tag for purposes of de-duping.
* Return a lowercase representation of the tag for purposes of de-duping.
*/
public function __toString(): string {
return strtolower($this->text);
}
/**
* Returns a lowercase representation of the tag for use in display.
* Return a lowercase representation of the tag for use in display.
*/
public function get_text(): string {
return strtolower($this->text);
}
/**
* Produce data used to serialize the tag.
*/
public function jsonSerialize(): mixed {
// Only used for passing to DOM
$details = get_object_vars($this);
@ -67,7 +108,7 @@
/**
* @param string[] $tag_array
* @return \CommunityTag[]
* @return CommunityTag[]
*/
private static function from_tag_array(array $tag_array) {
$tags = array_map(function(?string $tag) {
@ -86,10 +127,10 @@
}
/**
* Returns the user tags given, without any reserved tags.
* Constructs the tags given, removing any reserved tags.
* @param string[] $tags
* @param bool $remove_redundant Removes duplicate and obvious tags.
* @return \CommunityTag[]
* @return CommunityTag[]
*/
public static function from_user_tags(
array $tags, bool $remove_redundant = false
@ -105,7 +146,7 @@
if ($remove_redundant) {
$tags_built = CommunityTag::dedupe_tags($tags_built);
$tags_built = array_filter($tags_built, function(\CommunityTag $tag) {
$tags_built = array_filter($tags_built, function(CommunityTag $tag) {
$text = strtolower($tag->text);
return !in_array($text, CommunityTag::REDUNDANT_TAGS);
});
@ -116,20 +157,23 @@
/**
* @param string[] $details_array Array of string tags.
* @return \CommunityTag[]
* @return CommunityTag[]
*/
public static function from_details_array(array $details_array): array {
return CommunityTag::from_user_tags($details_array);
}
/**
* @param \CommunityTag[] $tags
* @return \CommunityTag[]
* @param CommunityTag[] $tags
* @return CommunityTag[]
*/
public static function dedupe_tags(array $tags) {
return array_unique($tags);
}
/**
* Return a HTML classname corresponding to the tag.
*/
public function get_tag_classname(): string {
$tag_type = $this->get_tag_type();
$classname = "room-label-$tag_type";
@ -142,6 +186,10 @@
return $classname;
}
/**
* Return a string representation of the tag's {@link TagType}.
* @return "user", "reserved", or "warning", as appropriate.
*/
public function get_tag_type(): string {
return match($this->type) {
TagType::USER_TAG => 'user',
@ -149,6 +197,7 @@
TagType::WARNING_TAG => 'warning'
};
}
/**
* @var string[] RESERVED_TAGS
* Array of derived tags unavailable for manual tagging.
@ -180,16 +229,22 @@
public const WARNING_ICON = "⚠️";
/**
* Checks whether the given manual tag can be accepted.
* Check whether the given user tag is reserved by our aggregator.
*/
public static function is_reserved_tag(string $tag): bool {
return in_array(strtolower($tag), CommunityTag::RESERVED_TAGS);
}
/**
* Return true if the tag should be given a chance to appear in more crowded views.
*/
public static function is_showcased_tag(string $tag): bool {
return in_array(strtolower($tag), CommunityTag::SHOWCASED_TAGS);
}
/**
* Return true if the tag should be given visibility in more crowded views.
*/
public static function is_highlighted_tag(string $tag): bool {
return in_array(strtolower($tag), CommunityTag::HIGHLIGHTED_TAGS);
}

@ -1,4 +1,8 @@
<?php
/**
* \file
* Execute all jobs: Fetch Community servers, generate HTML and Community listings.
*/
require_once 'fetch-servers.php';
require_once 'generate-html.php';
require_once 'generate-listings.php';

@ -1,28 +1,34 @@
<?php
/**
* \file
* Implement network coroutine execution.
*/
require_once 'utils/utils.php';
/**
* Class handling execution of network overhead coroutine.
*
* @template TReturn
*/
class FetchingCoroutine {
/**
* @var \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
* @var Generator<string,CurlHandle,CurlHandle|false,TReturn> $generator
*/
private Generator $generator;
private bool $consumed = false;
/**
* @var \Closure():bool $response_filter
* @var Closure():bool $response_filter
*/
private Closure $response_filter;
/**
* Creates a new Fetching Couroutine instance.
* @param \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
* Creates a new FetchingCouroutine instance.
* @param Generator<string,CurlHandle,CurlHandle|false,TReturn> $generator
* An instantiated generator yielding `string => CurlHandle` pairs.
*/
public function __construct(\Generator $generator) {
public function __construct(Generator $generator) {
$this->generator = $generator;
$this->response_filter = function(CurlHandle $handle): bool {
$code = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
@ -36,11 +42,11 @@
* Create a new FetchingCoroutine to fetch the contents of a URL.
* @param string $url URL to fetch.
* @param array $curlopts Addition cURL options.
* @return \FetchingCoroutine<CurlHandle|false> Coroutine returning
* @return FetchingCoroutine<CurlHandle|false> Coroutine returning
*/
public static function from_url(string $url, array $curlopts = []): \FetchingCoroutine {
public static function from_url(string $url, array $curlopts = []): FetchingCoroutine {
/**
* @var Generator<int,CurlHandle,CurlHandle|false,CurlHandle|false> $oneshot
* @var Generator<string,CurlHandle,CurlHandle|false,CurlHandle|false> $oneshot
*/
$oneshot = (function() use ($url, $curlopts) {
return yield make_curl_handle($url, $curlopts);
@ -49,11 +55,11 @@
}
/**
* Set callback deciding valid responses.
* Set a callback to decide successful responses.
* @param Closure $response_filter Predicate on a processed CurlHandle.
* @return \FetchingCoroutine
* @return FetchingCoroutine Return self.
*/
public function set_response_filter(Closure $response_filter): \FetchingCoroutine {
public function set_response_filter(Closure $response_filter): FetchingCoroutine {
$this->response_filter = $response_filter;
return $this;
}
@ -70,11 +76,11 @@
}
/**
* Modifies the current coroutine to halt on failed fetches. Consumes current coroutine.
* Resulting coroutine will not produce further fetches.
* @return \FetchingCoroutine<TReturn|null> New FetchingCoroutine instance.
* Produce a derived coroutine that halts on failed fetches. Consumes current coroutine.
* Resulting coroutine will not produce further fetches after failure.
* @return FetchingCoroutine<TReturn|null> New FetchingCoroutine instance.
*/
public function stop_on_failure(): \FetchingCoroutine {
public function stopping_on_failure(): FetchingCoroutine {
$this->consume();
$haltable = function () {
foreach ($this->generator as $id => $handle) {
@ -88,17 +94,18 @@
}
/**
* Modifies the current coroutine to retry fetches. Consumes current coroutine.
* Produce a derived coroutine that retries failed fetches a given number of times.
* Consumes current coroutine.
* @param int $retries Number of additional retries made for curl handles returned.
* @param bool $tallied_retries If true, the retry count applies to the whole coroutine.
* If false, each request is afforded the given retries.
* @return \FetchingCoroutine<TReturn> New FetchingCoroutine instance.
* @return FetchingCoroutine<TReturn> New FetchingCoroutine instance.
*/
public function retryable(int $retries, bool $tallied_retries = true): \FetchingCoroutine {
public function retryable(int $retries, bool $tallied_retries = true): FetchingCoroutine {
$this->consume();
$coroutine = $this;
$retryable = function () use ($retries, $coroutine, $tallied_retries) {
processing_new_coroutine:
processing_new_request:
while ($coroutine->valid()) {
$retries_current = $retries;
$id = $coroutine->current_key();
@ -108,7 +115,7 @@
if (!($attempt_handle = curl_copy_handle($handle))) {
log_error("Failed to clone cURL handle");
$coroutine->send(false);
goto processing_new_coroutine;
goto processing_new_request;
}
/** @var CurlHandle|false $response_handle */
@ -120,7 +127,7 @@
$url = curl_getinfo($response_handle, CURLINFO_EFFECTIVE_URL) ?? $url;
log_debug("Attempt #$attempt_no for $url returned code $retcode.");
$coroutine->send($response_handle);
goto processing_new_coroutine;
goto processing_new_request;
}
log_debug("Attempt #$attempt_no for $url failed or was rejected upstream.");
@ -142,12 +149,12 @@
}
/**
* Modifies the current coroutine to attempt HTTPS->HTTP downgrade after failure.
* Produces a derivedcoroutine that attempts HTTP downgrade on fetch failure.
* Consumes current coroutine.
* @param bool $did_downgrade Set to true if a downgrade to HTTP has taken place.
* @return \FetchingCoroutine<TReturn> New FetchingCoroutine instance.
* @return FetchingCoroutine<TReturn> New FetchingCoroutine instance.
*/
public function downgradeable(mixed &$did_downgrade = NULL): \FetchingCoroutine {
public function downgradeable(mixed &$did_downgrade = NULL): FetchingCoroutine {
$this->consume();
$coroutine = $this;
$has_downgrade_ref = func_num_args() >= 1;
@ -179,7 +186,7 @@
/**
* Assign non-generator parameters to given FetchingCoroutine.
*/
private function project_coroutine_parameters(\FetchingCoroutine $coroutine): \FetchingCoroutine {
private function project_coroutine_parameters(FetchingCoroutine $coroutine): FetchingCoroutine {
return $coroutine->set_response_filter($this->response_filter);
}
@ -189,14 +196,14 @@
}
/**
* Get the key of the handle yielded at this point in the coroutine, if applicable.
* Get the key yielded with the latest cURL handle by the coroutine, if applicable.
*/
public function current_key() {
public function current_key(): string {
return $this->generator->key();
}
/**
* Get the cURL handle yielded at this point in the coroutine, if applicable.
* Get the cURL handle yielded by the coroutine, if applicable.
*/
public function current_request(): CurlHandle|null {
return $this->generator->current();
@ -207,8 +214,8 @@
}
/**
* Invoke the current coroutine. Consumes coroutine.
* @return \Generator<int,CurlHandle,CurlHandle|false,TReturn>
* Invoke the coroutine and yield all resulting requests. Consumes coroutine.
* @return Generator<string,CurlHandle,CurlHandle|false,TReturn>
*/
public function run() {
$this->consume();
@ -217,7 +224,7 @@
}
/**
* Get the return value of the wrapped generator object once finished.
* Get the return value of the coroutine once finished.
* @return TReturn
*/
public function return_value(): mixed {
@ -225,12 +232,13 @@
}
/**
* Step coroutine until next yield point or end.
* Coroutine must not be consumed by any transformations.
* Step coroutine with network result until next yield point.
* Coroutine must not have been consumed by any transformations.
* @param CurlHandle|false $response
* Processed handle corresponding to yielded handle or false in case of failure.
* cURL handle containing fetch result or false in case of failure.
* @return bool True if response was accepted by coroutine, false otherwise.
*/
public function advance(CurlHandle|false $response_handle): bool {
public function advance(CurlHandle | false $response_handle): bool {
$this->assert_not_consumed();
return $this->send($response_handle);
}
@ -246,6 +254,9 @@
}
}
/**
* Class responsible for running multiple FetchingCoroutines concurrently.
*/
class FetchingCoroutineRunner {
/**
* Collection of enroled transfers.
@ -254,13 +265,13 @@
/**
* Coroutines executed by runner.
* @var \FetchingCoroutine[] $coroutines
* @var FetchingCoroutine[] $coroutines
*/
private array $coroutines;
/**
* Create new FetchingCoroutineRunner instance with the given coroutines.
* @param \FetchingCoroutine[] $coroutines Coroutines to run in parallel.
* Create a new FetchingCoroutineRunner instance on the given coroutines.
* @param FetchingCoroutine[] $coroutines Coroutines to run.
*/
public function __construct(array $coroutines = []) {
$this->coroutines = $coroutines;
@ -269,8 +280,8 @@
}
/**
* Launches all coroutines in parallel.
* @return int CURLM_* status.
* Launches all coroutines simultaneously.
* @return int CURLM_OK, or another curl_multi status in case of failure.
*/
public function run_all(): int {
do {
@ -315,7 +326,7 @@
while (false !== ($info = curl_multi_info_read($this->transfers))) {
if ($info['msg'] != CURLMSG_DONE) continue;
/**
* @var \CurlHandle $handle
* @var CurlHandle $handle
*/
$handle = $info['handle'];
curl_multi_remove_handle($this->transfers, $handle);

@ -1,4 +1,8 @@
<?php
/**
* \file
* Implement command-line option parsing.
*/
include_once 'utils/logging.php';
// Read the -v|--verbose option increasing logging verbosity to debug.

@ -1,4 +1,9 @@
<?php
/**
* \file
* Implement logging messages to console.
*/
/**
* @var int[] $hrtime_start
* Seconds and nanoseconds at start of logging period.

@ -0,0 +1,55 @@
<?php
/**
* \file
* Declare basic numeric constants and implement basic numeric operations.
*/
/**
* @var int $MINUTE_SECONDS
* Number of seconds in a minute.
*/
$MINUTE_SECONDS = 60;
/**
* @var int $HOUR_SECONDS
* Number of seconds in an hour.
*/
$HOUR_SECONDS = 60 * $MINUTE_SECONDS;
/**
* @var int $DAY_SECONDS
* Number of seconds in a day.
*/
$DAY_SECONDS = 24 * $HOUR_SECONDS;
/**
* @var int $WEEK_SECONDS
* Number of seconds in a week.
*/
$WEEK_SECONDS = 7 * $DAY_SECONDS;
/**
* Format a duration in seconds to human-readable format.
* @param int $duration_seconds Duration in seconds.
* @return string Duration string including number and unit.
*/
function format_duration(int $duration_seconds): string {
global $WEEK_SECONDS, $DAY_SECONDS, $HOUR_SECONDS, $MINUTE_SECONDS;
if ($duration_seconds >= $WEEK_SECONDS) {
return floor($duration_seconds / $WEEK_SECONDS) . ' week(s)';
}
if ($duration_seconds >= $DAY_SECONDS) {
return floor($duration_seconds / $DAY_SECONDS) . ' day(s)';
}
if ($duration_seconds >= $HOUR_SECONDS) {
return floor($duration_seconds / $HOUR_SECONDS) . ' hour(s)';
}
if ($duration_seconds >= $MINUTE_SECONDS) {
return floor($duration_seconds / $MINUTE_SECONDS) . 'minute(s)';
}
return floor($duration_seconds) . 's';
}
?>

@ -1,4 +1,9 @@
<?php
/**
* \file
* Implement basic utility functions.
*/
$REGEX_JOIN_LINK = (function(){
// See https://github.com/oxen-io/session-pysogs/blob/dev/administration.md
$protocol = 'https?:';
@ -11,14 +16,14 @@
/**
* Counts the total rooms across the given Community servers.
* @param \CommunityServer[] $servers Community Servers to count.
* @param CommunityServer[] $servers Community Servers to count.
* @return int Total number of Community rooms.
*/
function count_rooms(array $servers): int {
$rooms_total = 0;
foreach ($servers as $server) {
foreach ($server->rooms as $room) {
if (!$room->room_is_off_record()) {
if (!$room->is_off_record()) {
$rooms_total += 1;
}
}

@ -2,7 +2,7 @@
require_once 'php/assets/room-icons.php';
/**
* @var \CommunityRoom[] $rooms
* @var CommunityRoom[] $rooms
*/
$json_ld_data = array(
@ -10,7 +10,7 @@
'summary' => 'Active Session Communities',
'type' => 'Collection',
'totalItems' => count($rooms),
'items' => array_map(function(\CommunityRoom $room) {
'items' => array_map(function(CommunityRoom $room) {
$values = array(
'type' => 'Group',
'name' => $room->name,

@ -49,7 +49,7 @@
</tr>
<?php foreach ($rooms as $id => $room): ?>
<?php
if ($room->room_is_off_record()) {
if ($room->is_off_record()) {
// This can later allow SOGS
// to pass server-wide info using hidden dummy rooms.
continue;

Loading…
Cancel
Save