Reorganizing & codestyle compliance
parent
6fc91007a4
commit
ceea186ded
@ -0,0 +1,16 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path style="fill: grey;" d="M48 32C21.5 32 0 53.5 0 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H48zm80 64v64H64V96h64zM48 288c-26.5 0-48 21.5-48 48v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48zm80 64v64H64V352h64zM256 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H304c-26.5 0-48 21.5-48 48zm64 16h64v64H320V96zm32 352v32h32V448H352zm96 0H416v32h32V448zM416 288v32H352V288H256v96 96h64V384h32v32h96V352 320 288H416z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path style="fill: grey;" d="M48 32C21.5 32 0 53.5 0 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H48zm80 64v64H64V96h64zM48 288c-26.5 0-48 21.5-48 48v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V336c0-26.5-21.5-48-48-48H48zm80 64v64H64V352h64zM256 80v96c0 26.5 21.5 48 48 48h96c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48H304c-26.5 0-48 21.5-48 48zm64 16h64v64H320V96zm32 352v32h32V448H352zm96 0H416v32h32V448zM416 288v32H352V288H256v96 96h64V384h32v32h96V352 320 288H416z"/></svg>
|
||||
|
Before Width: | Height: | Size: 745 B After Width: | Height: | Size: 746 B |
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
require_once 'servers/known-servers.php';
|
||||
|
||||
/**
|
||||
* Return local path to room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
*/
|
||||
function room_icon_path(string $room_id): string {
|
||||
global $ROOM_ICONS_CACHE;
|
||||
return "$ROOM_ICONS_CACHE/$room_id";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return local path to resized room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
* @param string $size Image dimensions.
|
||||
*/
|
||||
function room_icon_path_resized(string $room_id, string $size): string {
|
||||
global $ROOM_ICONS;
|
||||
return "$ROOM_ICONS/$room_id-$size";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return server path to room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
* @param string $size Image dimensions.
|
||||
*/
|
||||
function room_icon_path_relative(string $room_id, string $size): string {
|
||||
global $ROOM_ICONS_RELATIVE;
|
||||
return "$ROOM_ICONS_RELATIVE/$room_id-$size";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<int,CurlHandle,CurlHandle|false,void>
|
||||
*/
|
||||
function fetch_room_icon_coroutine(\CommunityRoom $room): Generator {
|
||||
if (room_icon_safety($room) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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");
|
||||
|
||||
// Re-fetch icons periodically.
|
||||
if (!file_exists($icon_cached) || $icon_expired) {
|
||||
$icon_url = $room->get_icon_url();
|
||||
if (empty($icon_url)) {
|
||||
return null;
|
||||
}
|
||||
log_debug("Fetching icon for $room_id.");
|
||||
$icon_response = yield from FetchingCoroutine::from_url($icon_url)->run();
|
||||
$icon = $icon_response ? curl_multi_getcontent($icon_response) : null;
|
||||
if (empty($icon)) {
|
||||
log_info("$room_id returned an empty icon.");
|
||||
}
|
||||
// Never overwrite with an empty file.
|
||||
if (!(file_exists($icon_cached) && filesize($icon_cached) > 0 && empty($icon))) {
|
||||
file_put_contents($icon_cached, $icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the 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 {
|
||||
list($width, $height) = explode("x", $size);
|
||||
$width = intval($width);
|
||||
$height = intval($height);
|
||||
assert(!empty($width) && !empty($height));
|
||||
|
||||
if (room_icon_safety($room) < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$room_id = $room->get_room_identifier();
|
||||
$icon_cached = room_icon_path($room_id);
|
||||
$icon_resized = room_icon_path_resized($room_id, $size);
|
||||
$icon_expired = file_exists($icon_cached) && filemtime($icon_cached) < strtotime("-1 day");
|
||||
|
||||
if (!file_exists($icon_cached)) {
|
||||
log_debug("Missing icon asset for $room_id");
|
||||
return "";
|
||||
}
|
||||
if (!file_exists($icon_resized) || $icon_expired) {
|
||||
$icon_cached_contents = file_get_contents($icon_cached);
|
||||
if (empty($icon_cached_contents)) {
|
||||
file_put_contents($icon_resized, "");
|
||||
return "";
|
||||
}
|
||||
// Resize image
|
||||
$gd_image = imagecreatefromstring($icon_cached_contents);
|
||||
$gd_resized = imagescale($gd_image, $width, $height);
|
||||
if (!imagewebp($gd_resized, $icon_resized)) {
|
||||
log_info("Converting image for $room_id to $size failed");
|
||||
}
|
||||
}
|
||||
if (filesize($icon_resized) == 0) {
|
||||
return "";
|
||||
}
|
||||
return room_icon_path_relative($room_id, $size);
|
||||
}
|
||||
|
||||
function room_icon_safety(\CommunityRoom $room): int {
|
||||
global $ICON_ALLOWLIST, $ICON_BLOCKLIST;
|
||||
if (in_array($room->get_room_identifier(), $ICON_BLOCKLIST)) {
|
||||
return -1;
|
||||
}
|
||||
if (in_array($room->server->get_hostname(), $ICON_ALLOWLIST)) {
|
||||
return 1;
|
||||
}
|
||||
if (in_array($room->server->get_hostname(), $ICON_BLOCKLIST)) {
|
||||
return -1;
|
||||
}
|
||||
if ($room->has_nsfw_keywords()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
file_exists($ROOM_ICONS_CACHE) or mkdir($ROOM_ICONS_CACHE, 0755, true);
|
||||
file_exists($ROOM_ICONS) or mkdir($ROOM_ICONS, 0755, true);
|
||||
?>
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
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 string $size Image dimensions.
|
||||
* @return string Relative path or null if icon is absent.
|
||||
*/
|
||||
function server_icon(\CommunityServer $server, string $size): ?string {
|
||||
global $SERVER_ICON_MAPPING;
|
||||
$hostname = $server->get_hostname();
|
||||
if (!isset($SERVER_ICON_MAPPING[$hostname])) {
|
||||
return "";
|
||||
}
|
||||
$room_token = $SERVER_ICON_MAPPING[$hostname];
|
||||
$room = $server->get_room_by_token($room_token);
|
||||
if (!$room) {
|
||||
log_warning("Room $room_token on $hostname does not exist, cannot be used as icon.");
|
||||
return "";
|
||||
}
|
||||
return room_icon($room, $size);
|
||||
}
|
||||
?>
|
@ -1,121 +1,121 @@
|
||||
<?php
|
||||
require_once "getenv.php";
|
||||
require_once "php/utils/servers-rooms.php";
|
||||
require_once "php/utils/logging.php";
|
||||
require_once "getenv.php";
|
||||
require_once "utils/logging.php";
|
||||
require_once "servers/servers-rooms.php";
|
||||
|
||||
class CommunityListing implements JsonSerializable {
|
||||
public readonly string $id;
|
||||
public readonly string $name;
|
||||
public readonly string $rating;
|
||||
public readonly array $rooms;
|
||||
class CommunityListing implements JsonSerializable {
|
||||
public readonly string $id;
|
||||
public readonly string $name;
|
||||
public readonly string $rating;
|
||||
public readonly array $rooms;
|
||||
|
||||
/**
|
||||
* @param \CommunityRoom[] $rooms
|
||||
*/
|
||||
public function __construct(string $id, string $name, ?string $rating, array $rooms) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->rating = $rating ?? "unknown";
|
||||
$this->rooms = $rooms;
|
||||
}
|
||||
/**
|
||||
* @param \CommunityRoom[] $rooms
|
||||
*/
|
||||
public function __construct(string $id, string $name, ?string $rating, array $rooms) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->rating = $rating ?? "unknown";
|
||||
$this->rooms = $rooms;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed {
|
||||
// TODO: Careful serialization
|
||||
$details = get_object_vars($this);
|
||||
$details['rooms'] = array_map(function(\CommunityRoom $room){
|
||||
return $room->to_listing_data();
|
||||
}, $this->rooms);
|
||||
return $details;
|
||||
}
|
||||
public function jsonSerialize(): mixed {
|
||||
// TODO: Careful serialization
|
||||
$details = get_object_vars($this);
|
||||
$details['rooms'] = array_map(function(\CommunityRoom $room){
|
||||
return $room->to_listing_data();
|
||||
}, $this->rooms);
|
||||
return $details;
|
||||
}
|
||||
|
||||
public function to_summary(): array {
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'rating' => $this->rating,
|
||||
'rooms' => count($this->rooms)
|
||||
);
|
||||
}
|
||||
}
|
||||
public function to_summary(): array {
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'rating' => $this->rating,
|
||||
'rooms' => count($this->rooms)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \CommunityListing[]
|
||||
*/
|
||||
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_by_id = [];
|
||||
foreach (CommunityServer::enumerate_rooms($servers) 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 = [];
|
||||
if (isset($listing_props['rooms'])) {
|
||||
foreach ($listing_props['rooms'] as $room_id) {
|
||||
if (isset($rooms_by_id[$room_id])) {
|
||||
$rooms[] = $rooms_by_id[$room_id];
|
||||
} else {
|
||||
log_warning("Could not find room $room_id from listing $id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($listing_props['sogs'])) {
|
||||
foreach ($listing_props['sogs'] as $public_key) {
|
||||
if (isset($sogs_by_pubkey[$public_key])) {
|
||||
/** @var \CommunityServer $sogs */
|
||||
$sogs = $sogs_by_pubkey[$public_key];
|
||||
array_push($rooms, ...$sogs->rooms);
|
||||
} else {
|
||||
log_warning("Could not find sogs $public_key from listing $id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$listings[] = new CommunityListing(
|
||||
$id,
|
||||
$listing_props['name'],
|
||||
$listing_props['rating'],
|
||||
$rooms
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @return \CommunityListing[]
|
||||
*/
|
||||
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_by_id = [];
|
||||
foreach (CommunityServer::enumerate_rooms($servers) 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 = [];
|
||||
if (isset($listing_props['rooms'])) {
|
||||
foreach ($listing_props['rooms'] as $room_id) {
|
||||
if (isset($rooms_by_id[$room_id])) {
|
||||
$rooms[] = $rooms_by_id[$room_id];
|
||||
} else {
|
||||
log_warning("Could not find room $room_id from listing $id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($listing_props['sogs'])) {
|
||||
foreach ($listing_props['sogs'] as $public_key) {
|
||||
if (isset($sogs_by_pubkey[$public_key])) {
|
||||
/** @var \CommunityServer $sogs */
|
||||
$sogs = $sogs_by_pubkey[$public_key];
|
||||
array_push($rooms, ...$sogs->rooms);
|
||||
} else {
|
||||
log_warning("Could not find sogs $public_key from listing $id.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$listings[] = new CommunityListing(
|
||||
$id,
|
||||
$listing_props['name'],
|
||||
$listing_props['rating'],
|
||||
$rooms
|
||||
);
|
||||
}
|
||||
|
||||
return $listings;
|
||||
}
|
||||
return $listings;
|
||||
}
|
||||
|
||||
function generate_listings() {
|
||||
global $LISTING_PROVIDER_LISTING_SUMMARY, $LISTING_PROVIDER_LISTINGS;
|
||||
log_info("Generating listings...");
|
||||
function generate_listings() {
|
||||
global $LISTING_PROVIDER_LISTING_SUMMARY, $LISTING_PROVIDER_LISTINGS;
|
||||
log_info("Generating listings...");
|
||||
|
||||
$listings_resolved = resolve_listings_config();
|
||||
log_value($listings_resolved);
|
||||
$summaries = array_map(function(\CommunityListing $listing) {
|
||||
return $listing->to_summary();
|
||||
}, $listings_resolved);
|
||||
file_put_contents($LISTING_PROVIDER_LISTING_SUMMARY, json_encode($summaries));
|
||||
foreach ($listings_resolved as $listing) {
|
||||
$id = $listing->id;
|
||||
file_put_contents(
|
||||
"$LISTING_PROVIDER_LISTINGS/$id",
|
||||
json_encode($listing)
|
||||
);
|
||||
}
|
||||
$listings_count = count($listings_resolved);
|
||||
log_info("Generated $listings_count listings.");
|
||||
}
|
||||
$listings_resolved = resolve_listings_config();
|
||||
log_value($listings_resolved);
|
||||
$summaries = array_map(function(\CommunityListing $listing) {
|
||||
return $listing->to_summary();
|
||||
}, $listings_resolved);
|
||||
file_put_contents($LISTING_PROVIDER_LISTING_SUMMARY, json_encode($summaries));
|
||||
foreach ($listings_resolved as $listing) {
|
||||
$id = $listing->id;
|
||||
file_put_contents(
|
||||
"$LISTING_PROVIDER_LISTINGS/$id",
|
||||
json_encode($listing)
|
||||
);
|
||||
}
|
||||
$listings_count = count($listings_resolved);
|
||||
log_info("Generated $listings_count listings.");
|
||||
}
|
||||
|
||||
file_exists($LISTING_PROVIDER_LISTINGS) or mkdir($LISTING_PROVIDER_LISTINGS, 0755, true);
|
||||
file_exists($LISTING_PROVIDER_LISTINGS) or mkdir($LISTING_PROVIDER_LISTINGS, 0755, true);
|
||||
|
||||
$options = getopt("v", ["verbose"]);
|
||||
if (isset($options["v"]) or isset($options["verbose"])) {
|
||||
$LOGGING_VERBOSITY = LoggingVerbosity::Debug;
|
||||
}
|
||||
$options = getopt("v", ["verbose"]);
|
||||
if (isset($options["v"]) or isset($options["verbose"])) {
|
||||
$LOGGING_VERBOSITY = LoggingVerbosity::Debug;
|
||||
}
|
||||
|
||||
generate_listings();
|
||||
?>
|
||||
generate_listings();
|
||||
?>
|
||||
|
@ -0,0 +1,318 @@
|
||||
<?php
|
||||
require_once 'utils/utils.php';
|
||||
require_once 'servers/tags.php';
|
||||
|
||||
class SDIRCommunitySource {
|
||||
private function __construct(string $contents) {
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance of this source from contents.
|
||||
* Returns false if processing the source fails.
|
||||
* @return \SDIRCommunitySource|false
|
||||
*/
|
||||
public static function from_contents(string $contents) {
|
||||
$source = new SDIRCommunitySource($contents);
|
||||
|
||||
if (!$source->sdir_process_tags()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private readonly string $contents;
|
||||
|
||||
/**
|
||||
* @var string[][] $tags Array associating room IDs with string tag arrays.
|
||||
*/
|
||||
private array $tags;
|
||||
|
||||
private static function sdir_validate_entry(
|
||||
array $room_entry,
|
||||
bool &$missing_url,
|
||||
bool &$missing_tags
|
||||
): bool {
|
||||
if (!isset($room_entry['url']) || !is_string($room_entry['url'])) {
|
||||
log_value($room_entry);
|
||||
$missing_url = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($room_entry['tags']) || !is_string($room_entry['tags'])) {
|
||||
log_value($room_entry);
|
||||
$missing_tags = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function sdir_report_errors(bool $entry_missing_url, bool $entry_missing_tags) {
|
||||
if ($entry_missing_url) {
|
||||
log_error("One or more room entries from session.directory is missing the 'url' parameter.");
|
||||
}
|
||||
|
||||
if ($entry_missing_tags) {
|
||||
log_error("One or more room entries from session.directory is missing the 'tags' parameter.");
|
||||
}
|
||||
}
|
||||
|
||||
private function get_sdir_entries(): array|bool {
|
||||
try {
|
||||
return json_decode($this->contents, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function sdir_process_tags(): bool {
|
||||
$entry_missing_url = false;
|
||||
$entry_missing_tags = false;
|
||||
|
||||
$rooms = SDIRCommunitySource::get_sdir_entries($this->contents);
|
||||
|
||||
if (!$rooms) {
|
||||
log_error("Could not parse entries from session.directory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rooms as $room_entry) {
|
||||
if (!SDIRCommunitySource::sdir_validate_entry(
|
||||
$room_entry, $entry_missing_url, $entry_missing_tags
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$url = $room_entry['url'];
|
||||
$tags = $room_entry['tags'];
|
||||
|
||||
$room_id = url_get_room_id($url);
|
||||
|
||||
$this->tags[$room_id] = explode(',', $tags);
|
||||
}
|
||||
|
||||
SDIRCommunitySource::sdir_report_errors($entry_missing_url, $entry_missing_tags);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][] Array associating room IDs with string tag arrays.
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
||||
|
||||
class ASGLCommunitySource {
|
||||
private function __construct(string $contents) {
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
private readonly string $contents;
|
||||
|
||||
/**
|
||||
* @var string[][] $tags;
|
||||
*/
|
||||
private array $tags = [];
|
||||
|
||||
/**
|
||||
* @return \ASGLCommunitySource|false
|
||||
*/
|
||||
public static function from_contents(string $contents) {
|
||||
$source = new ASGLCommunitySource($contents);
|
||||
|
||||
if(!$source->asgl_process_tags()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private function asgl_process_tags(): bool {
|
||||
$lines = explode("\n", $this->contents);
|
||||
// $last_headings = [];
|
||||
$last_room_id = null;
|
||||
$room_tags = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
ASGLCommunitySource::asgl_process_line($line, $last_room_id, $room_tags);
|
||||
|
||||
if ($last_room_id != null && count($room_tags) > 0) {
|
||||
$this->tags[$last_room_id] = $room_tags;
|
||||
$last_room_id = null;
|
||||
$room_tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function asgl_process_line(
|
||||
?string $line,
|
||||
?string &$last_room_id,
|
||||
array &$room_tags
|
||||
) {
|
||||
$line = trim($line);
|
||||
|
||||
if (strlen($line) == 0) {
|
||||
$last_room_id = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$urls = parse_join_links($line);
|
||||
|
||||
if (count($urls) == 1 && $urls[0] == $line) {
|
||||
$last_room_id = url_get_room_id($urls[0]);
|
||||
$room_tags = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, "hashtag")) {
|
||||
$room_tags = ASGLCommunitySource::read_asgl_tags($line);
|
||||
}
|
||||
}
|
||||
|
||||
private static function read_asgl_tags(string $line): array {
|
||||
$matches = [];
|
||||
preg_match_all('/`#([^`]+)`/', $line, $matches);
|
||||
// Return first group matches.
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
public function get_tags(): array {
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
||||
|
||||
class CommunitySources {
|
||||
private const SOURCES = array(
|
||||
'ASGL' => 'https://raw.githubusercontent.com/GNU-Linux-libre/Awesome-Session-Group-List/main/README.md',
|
||||
'LOKI' => 'https://lokilocker.com/Mods/Session-Groups/wiki/Session-Open-Groups',
|
||||
'SDIR' => 'https://session.directory/?all=groups',
|
||||
'SDIR-BASE' => 'https://session.directory/',
|
||||
'SDIR-PATTERN' => '/view_session_group_user_lokinet\.php\?id=\d+/',
|
||||
'SDIR-JSON' => 'https://session.directory/scrape.php',
|
||||
'FARK' => 'https://freearkham.cc/'
|
||||
);
|
||||
|
||||
private readonly string $contents_asgl;
|
||||
private readonly string $contents_loki;
|
||||
private readonly string $contents_sdir;
|
||||
private readonly string $contents_fark;
|
||||
private readonly string $contents_aggregated;
|
||||
|
||||
/**
|
||||
* Arraying associating room identifiers with arrays of raw tags.
|
||||
* @var array<string,string[]> $room_tags
|
||||
*/
|
||||
private array $room_tags = [];
|
||||
|
||||
/**
|
||||
* Fetches and saves known sources of Session Community join links.
|
||||
*/
|
||||
public function __construct() {
|
||||
log_info("Requesting Awesome Session Group list...");
|
||||
$this->contents_asgl = CommunitySources::fetch_source('ASGL');
|
||||
|
||||
log_info("Requesting Lokilocker Mods Open Group list...");
|
||||
$this->contents_loki = CommunitySources::fetch_source('LOKI');
|
||||
|
||||
log_info("Requesting session.directory list...");
|
||||
$this->contents_sdir = CommunitySources::fetch_source('SDIR-JSON');
|
||||
|
||||
log_info("Requesting FreeArkham.cc list...");
|
||||
$this->contents_fark = CommunitySources::fetch_source('FARK');
|
||||
|
||||
log_info("Parsing extra information...");
|
||||
if (!$this->process_sources()) {
|
||||
log_error("Could not parse extra information from one or more sources.");
|
||||
}
|
||||
|
||||
log_info('Done fetching sources.');
|
||||
|
||||
$this->contents_aggregated =
|
||||
$this->contents_asgl .
|
||||
$this->contents_fark .
|
||||
$this->contents_loki .
|
||||
// Slashes are escaped when served, unescape them
|
||||
str_replace("\\/", "/", $this->contents_sdir);
|
||||
}
|
||||
|
||||
private static function fetch_source(string $source_key) {
|
||||
$url = CommunitySources::SOURCES[$source_key];
|
||||
|
||||
$contents = file_get_contents($url);
|
||||
log_debug($http_response_header[0]);
|
||||
|
||||
if (!$contents) {
|
||||
log_error("Could not fetch source from $url.");
|
||||
return "";
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[][] $tags Array associating room IDs to tag arrays
|
||||
*/
|
||||
private function add_tags(array $tags) {
|
||||
foreach ($tags as $room_id => $room_tags) {
|
||||
if (!isset($this->room_tags[$room_id])) {
|
||||
$this->room_tags[$room_id] = [];
|
||||
}
|
||||
|
||||
$this->room_tags[$room_id] = [
|
||||
...$this->room_tags[$room_id],
|
||||
...$room_tags
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function process_sources(): bool {
|
||||
$source_sdir = SDIRCommunitySource::from_contents($this->contents_sdir);
|
||||
|
||||
$source_asgl = ASGLCommunitySource::from_contents($this->contents_asgl);
|
||||
|
||||
$source_sdir && $this->add_tags($source_sdir->get_tags());
|
||||
|
||||
$source_asgl && $this->add_tags($source_asgl->get_tags());
|
||||
|
||||
if (!$source_sdir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$source_asgl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all join URLs found.
|
||||
* @return string[] Join URLs.
|
||||
*/
|
||||
public function get_join_urls(): array {
|
||||
return array_unique(
|
||||
parse_join_links($this->contents_aggregated)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return known tags for the given room.
|
||||
* @param string $room_id Room identifier.
|
||||
* @return \CommunityTag[] Array of string tags.
|
||||
*/
|
||||
public function get_room_tags($room_id): array {
|
||||
if (!isset($this->room_tags[$room_id])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->room_tags[$room_id];
|
||||
}
|
||||
}
|
||||
?>
|
@ -0,0 +1,185 @@
|
||||
<?php
|
||||
require_once 'utils/utils.php';
|
||||
|
||||
class TagType {
|
||||
private function __construct() {}
|
||||
const USER_TAG = 0;
|
||||
const RESERVED_TAG = 1;
|
||||
const WARNING_TAG = 2;
|
||||
}
|
||||
|
||||
class CommunityTag implements JsonSerializable {
|
||||
public function __construct(
|
||||
string $text,
|
||||
int $tag_type = TagType::USER_TAG,
|
||||
string $description = ""
|
||||
) {
|
||||
$this->text = $text;
|
||||
$this->type = $tag_type;
|
||||
$this->description =
|
||||
empty($description) ? "Tag: $text" : $description;
|
||||
}
|
||||
|
||||
public readonly int $type;
|
||||
|
||||
public readonly string $text;
|
||||
|
||||
public readonly string $description;
|
||||
|
||||
/**
|
||||
* Returns 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.
|
||||
*/
|
||||
public function get_text(): string {
|
||||
return strtolower($this->text);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed {
|
||||
// Only used for passing to DOM
|
||||
$details = get_object_vars($this);
|
||||
$details['text'] = html_sanitize($this->get_text());
|
||||
$details['description'] = html_sanitize($details['description']);
|
||||
$details['type'] = $this->get_tag_type();
|
||||
return $details;
|
||||
}
|
||||
|
||||
private static function preprocess_tag(?string $tag) {
|
||||
$tag = trim($tag);
|
||||
|
||||
if (strlen($tag) == 0) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$tag = html_sanitize(html_entity_decode($tag));
|
||||
|
||||
if ($tag[0] == '#') {
|
||||
return substr($tag, 1);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tag_array
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
private static function from_tag_array(array $tag_array) {
|
||||
$tags = array_map(function(?string $tag) {
|
||||
return CommunityTag::preprocess_tag($tag);
|
||||
}, $tag_array);
|
||||
|
||||
$tags = array_filter(
|
||||
$tags, function(?string $tag) {
|
||||
return strlen($tag) != 0;
|
||||
}
|
||||
);
|
||||
|
||||
return array_map(function(string $tag) {
|
||||
return new CommunityTag($tag);
|
||||
}, $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user tags given, without any reserved tags.
|
||||
* @param string[] $tags
|
||||
* @param bool $remove_redundant Removes duplicate and obvious tags.
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function from_user_tags(
|
||||
array $tags, bool $remove_redundant = false
|
||||
): array {
|
||||
$tags_user = array_filter(
|
||||
$tags,
|
||||
function($tag) {
|
||||
return !CommunityTag::is_reserved_tag($tag);
|
||||
}
|
||||
);
|
||||
|
||||
$tags_built = CommunityTag::from_tag_array($tags_user);
|
||||
|
||||
if ($remove_redundant) {
|
||||
$tags_built = CommunityTag::dedupe_tags($tags_built);
|
||||
$tags_built = array_filter($tags_built, function(\CommunityTag $tag) {
|
||||
$text = strtolower($tag->text);
|
||||
return !in_array($text, CommunityTag::REDUNDANT_TAGS);
|
||||
});
|
||||
}
|
||||
|
||||
return $tags_built;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $details_array Array of string tags.
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function from_details_array(array $details_array): array {
|
||||
return CommunityTag::from_user_tags($details_array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \CommunityTag[] $tags
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function dedupe_tags(array $tags) {
|
||||
return array_unique($tags);
|
||||
}
|
||||
|
||||
public function get_tag_classname(): string {
|
||||
$tag_type = $this->get_tag_type();
|
||||
$classname = "room-label-$tag_type";
|
||||
if (CommunityTag::is_showcased_tag($this->text)) {
|
||||
$classname .= " room-label-showcased";
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
public function get_tag_type(): string {
|
||||
return match($this->type) {
|
||||
TagType::USER_TAG => 'user',
|
||||
TagType::RESERVED_TAG => 'reserved',
|
||||
TagType::WARNING_TAG => 'warning'
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @var string[] RESERVED_TAGS
|
||||
* Array of derived tags unavailable for manual tagging.
|
||||
*/
|
||||
private const RESERVED_TAGS = [
|
||||
"official",
|
||||
"nsfw",
|
||||
"new",
|
||||
"modded",
|
||||
"not modded",
|
||||
"read-only",
|
||||
"uploads off",
|
||||
"we're here"
|
||||
];
|
||||
|
||||
private const SHOWCASED_TAGS = ["official", "new", "we're here"];
|
||||
|
||||
private const REDUNDANT_TAGS = ["session"];
|
||||
|
||||
public const NSFW_KEYWORDS = ["nsfw", "porn", "erotic", "18+"];
|
||||
|
||||
public const CHECK_MARK = "✅";
|
||||
|
||||
public const WARNING_ICON = "⚠️";
|
||||
|
||||
/**
|
||||
* Checks whether the given manual tag can be accepted.
|
||||
*/
|
||||
public static function is_reserved_tag(string $tag): bool {
|
||||
return in_array(strtolower($tag), CommunityTag::RESERVED_TAGS);
|
||||
}
|
||||
|
||||
public static function is_showcased_tag(string $tag): bool {
|
||||
return in_array(strtolower($tag), CommunityTag::SHOWCASED_TAGS);
|
||||
}
|
||||
}
|
||||
?>
|
@ -1,332 +1,332 @@
|
||||
<?php
|
||||
require_once 'utils.php';
|
||||
|
||||
/**
|
||||
* @template TReturn
|
||||
*/
|
||||
class FetchingCoroutine {
|
||||
/**
|
||||
* @var \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
|
||||
*/
|
||||
private Generator $generator;
|
||||
|
||||
private bool $consumed = false;
|
||||
|
||||
/**
|
||||
* @var \Closure():bool $response_filter
|
||||
*/
|
||||
private Closure $response_filter;
|
||||
|
||||
/**
|
||||
* Creates a new Fetching Couroutine instance.
|
||||
* @param \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
|
||||
* An instantiated generator yielding `string => CurlHandle` pairs.
|
||||
*/
|
||||
public function __construct(\Generator $generator) {
|
||||
$this->generator = $generator;
|
||||
$this->response_filter = function(CurlHandle $handle): bool {
|
||||
$code = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
|
||||
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
|
||||
log_debug("Got code $code for $url in default request arbitrator.");
|
||||
return $code < 300 && $code != 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function from_url(string $url, array $curlopts = []): \FetchingCoroutine {
|
||||
/**
|
||||
* @var Generator<int,CurlHandle,CurlHandle|false,CurlHandle|false> $oneshot
|
||||
*/
|
||||
$oneshot = (function() use ($url, $curlopts) {
|
||||
return yield make_curl_handle($url, $curlopts);
|
||||
})();
|
||||
return new FetchingCoroutine($oneshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback deciding valid responses.
|
||||
* @param Closure $response_filter Predicate on a processed CurlHandle.
|
||||
* @return \FetchingCoroutine
|
||||
*/
|
||||
public function set_response_filter(Closure $response_filter): \FetchingCoroutine {
|
||||
$this->response_filter = $response_filter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function assert_not_consumed() {
|
||||
if ($this->consumed) {
|
||||
throw new Error("This FetchingCoroutine has been used up by a transforming call");
|
||||
}
|
||||
}
|
||||
|
||||
private function consume() {
|
||||
$this->assert_not_consumed();
|
||||
$this->consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function stop_on_failure(): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$haltable = function () {
|
||||
foreach ($this->generator as $id => $handle) {
|
||||
if (!(yield $id => $handle)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return $this->generator->getReturn();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($haltable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the current coroutine to retry fetches. 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.
|
||||
*/
|
||||
public function retryable(int $retries, bool $tallied_retries = true): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$coroutine = $this;
|
||||
$retryable = function () use ($retries, $coroutine, $tallied_retries) {
|
||||
processing_new_coroutine:
|
||||
while ($coroutine->valid()) {
|
||||
$retries_current = $retries;
|
||||
$id = $coroutine->current_key();
|
||||
$handle = $coroutine->current_request();
|
||||
$attempt_no = 1;
|
||||
do {
|
||||
if (!($attempt_handle = curl_copy_handle($handle))) {
|
||||
log_error("Failed to clone cURL handle");
|
||||
$coroutine->send(false);
|
||||
goto processing_new_coroutine;
|
||||
}
|
||||
|
||||
/** @var CurlHandle|false $response_handle */
|
||||
$response_handle = yield $id => $attempt_handle;
|
||||
$url = curl_getinfo($attempt_handle, CURLINFO_EFFECTIVE_URL);
|
||||
|
||||
if ($response_handle) {
|
||||
$retcode = curl_getinfo($response_handle, CURLINFO_HTTP_CODE);
|
||||
$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;
|
||||
}
|
||||
|
||||
log_debug("Attempt #$attempt_no for $url failed or was rejected upstream.");
|
||||
|
||||
$attempt_no++;
|
||||
} while ($retries_current-- > 0);
|
||||
|
||||
// failed to fetch handle
|
||||
$coroutine->send(false);
|
||||
|
||||
// decrease the remaining retries
|
||||
if ($tallied_retries) {
|
||||
$retries = $retries_current;
|
||||
}
|
||||
}
|
||||
return $coroutine->return_value();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($retryable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the current coroutine to attempt HTTPS->HTTP downgrade after 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.
|
||||
*/
|
||||
public function downgradeable(mixed &$did_downgrade = NULL): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$coroutine = $this;
|
||||
$has_downgrade_ref = func_num_args() >= 1;
|
||||
if ($has_downgrade_ref) $did_downgrade = false;
|
||||
$downgradeable = function () use ($coroutine, &$did_downgrade, $has_downgrade_ref) {
|
||||
while ($coroutine->valid()) {
|
||||
$id = $coroutine->current_key();
|
||||
$handle = $coroutine->current_request();
|
||||
$handle_downgraded = curl_handle_downgrade($handle);
|
||||
// Try HTTPS first
|
||||
if ($handle_downgraded) {
|
||||
// Skip to next handle on success
|
||||
if ($coroutine->send(yield $id => $handle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($has_downgrade_ref) $did_downgrade = true;
|
||||
$handle = $handle_downgraded;
|
||||
}
|
||||
|
||||
// Use HTTP
|
||||
$coroutine->send(yield $id => $handle);
|
||||
}
|
||||
return $coroutine->return_value();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($downgradeable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign non-generator parameters to given FetchingCoroutine.
|
||||
*/
|
||||
private function project_coroutine_parameters(\FetchingCoroutine $coroutine): \FetchingCoroutine {
|
||||
return $coroutine->set_response_filter($this->response_filter);
|
||||
}
|
||||
|
||||
private function is_valid_response(CurlHandle $handle) {
|
||||
$response_filter = $this->response_filter;
|
||||
return $response_filter($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key of the handle yielded at this point in the coroutine, if applicable.
|
||||
*/
|
||||
public function current_key() {
|
||||
return $this->generator->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cURL handle yielded at this point in the coroutine, if applicable.
|
||||
*/
|
||||
public function current_request(): CurlHandle|null {
|
||||
return $this->generator->current();
|
||||
}
|
||||
|
||||
private function valid(): bool {
|
||||
return $this->generator->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the current coroutine. Consumes coroutine.
|
||||
* @return \Generator<int,CurlHandle,CurlHandle|false,TReturn>
|
||||
*/
|
||||
public function run() {
|
||||
$this->consume();
|
||||
// passthrough
|
||||
return yield from $this->generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the return value of the wrapped generator object once finished.
|
||||
* @return TReturn
|
||||
*/
|
||||
public function return_value(): mixed {
|
||||
return $this->generator->getReturn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step coroutine until next yield point or end.
|
||||
* Coroutine must not be consumed by any transformations.
|
||||
* @param CurlHandle|false $response
|
||||
* Processed handle corresponding to yielded handle or false in case of failure.
|
||||
*/
|
||||
public function advance(CurlHandle|false $response_handle): bool {
|
||||
$this->assert_not_consumed();
|
||||
return $this->send($response_handle);
|
||||
}
|
||||
|
||||
private function send(CurlHandle|false $handle): bool {
|
||||
if ($handle && $this->is_valid_response($handle)) {
|
||||
$this->generator->send($handle);
|
||||
return true;
|
||||
} else {
|
||||
$this->generator->send(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FetchingCoroutineRunner {
|
||||
/**
|
||||
* Collection of enroled transfers.
|
||||
*/
|
||||
private CurlMultiHandle $transfers;
|
||||
|
||||
/**
|
||||
* Coroutines executed by runner.
|
||||
* @var \FetchingCoroutine[] $coroutines
|
||||
*/
|
||||
private array $coroutines;
|
||||
|
||||
/**
|
||||
* Create new FetchingCoroutineRunner instance with the given coroutines.
|
||||
* @param \FetchingCoroutine[] $coroutines Coroutines to run in parallel.
|
||||
*/
|
||||
public function __construct(array $coroutines = []) {
|
||||
$this->coroutines = $coroutines;
|
||||
|
||||
$this->initialize_coroutines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches all coroutines in parallel.
|
||||
* @return int CURLM_* status.
|
||||
*/
|
||||
public function run_all(): int {
|
||||
do {
|
||||
$curlm_status = curl_multi_exec($this->transfers, $curlm_active_transfer);
|
||||
if ($curlm_active_transfer) {
|
||||
// Block 1 second for pending transfers
|
||||
curl_multi_select($this->transfers, timeout: 1.0);
|
||||
// curl_multi_select($transfers, timeout: 6.0);
|
||||
}
|
||||
$this->process_curl_activity();
|
||||
} while ($curlm_active_transfer && $curlm_status == CURLM_OK);
|
||||
|
||||
return $curlm_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrol initial transfers from all coroutines.
|
||||
*/
|
||||
private function initialize_coroutines() {
|
||||
$this->transfers = curl_multi_init();
|
||||
|
||||
foreach ($this->coroutines as $id => $coroutine) {
|
||||
$this->poll_coroutine_for_transfer($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrol latest transfer from coroutine with given id.
|
||||
*/
|
||||
private function poll_coroutine_for_transfer(int $id) {
|
||||
$coroutine = $this->coroutines[$id];
|
||||
$handle = $coroutine->current_request();
|
||||
if (!$handle) return;
|
||||
curl_setopt($handle, CURLOPT_PRIVATE, $id);
|
||||
curl_multi_add_handle($this->transfers, $handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to new activity on enroled transfers.
|
||||
*/
|
||||
private function process_curl_activity() {
|
||||
while (false !== ($info = curl_multi_info_read($this->transfers))) {
|
||||
if ($info['msg'] != CURLMSG_DONE) continue;
|
||||
/**
|
||||
* @var \CurlHandle $handle
|
||||
*/
|
||||
$handle = $info['handle'];
|
||||
curl_multi_remove_handle($this->transfers, $handle);
|
||||
$coroutine_id = curl_getinfo($handle, CURLINFO_PRIVATE);
|
||||
if (!isset($this->coroutines[$coroutine_id])) {
|
||||
throw new Error("Invalid coroutine ID: " + $coroutine_id);
|
||||
}
|
||||
$this->coroutines[$coroutine_id]->advance($handle);
|
||||
$this->poll_coroutine_for_transfer($coroutine_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once 'utils/utils.php';
|
||||
|
||||
/**
|
||||
* @template TReturn
|
||||
*/
|
||||
class FetchingCoroutine {
|
||||
/**
|
||||
* @var \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
|
||||
*/
|
||||
private Generator $generator;
|
||||
|
||||
private bool $consumed = false;
|
||||
|
||||
/**
|
||||
* @var \Closure():bool $response_filter
|
||||
*/
|
||||
private Closure $response_filter;
|
||||
|
||||
/**
|
||||
* Creates a new Fetching Couroutine instance.
|
||||
* @param \Generator<int,CurlHandle,CurlHandle|false,TReturn> $generator
|
||||
* An instantiated generator yielding `string => CurlHandle` pairs.
|
||||
*/
|
||||
public function __construct(\Generator $generator) {
|
||||
$this->generator = $generator;
|
||||
$this->response_filter = function(CurlHandle $handle): bool {
|
||||
$code = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
|
||||
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
|
||||
log_debug("Got code $code for $url in default request arbitrator.");
|
||||
return $code < 300 && $code != 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function from_url(string $url, array $curlopts = []): \FetchingCoroutine {
|
||||
/**
|
||||
* @var Generator<int,CurlHandle,CurlHandle|false,CurlHandle|false> $oneshot
|
||||
*/
|
||||
$oneshot = (function() use ($url, $curlopts) {
|
||||
return yield make_curl_handle($url, $curlopts);
|
||||
})();
|
||||
return new FetchingCoroutine($oneshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback deciding valid responses.
|
||||
* @param Closure $response_filter Predicate on a processed CurlHandle.
|
||||
* @return \FetchingCoroutine
|
||||
*/
|
||||
public function set_response_filter(Closure $response_filter): \FetchingCoroutine {
|
||||
$this->response_filter = $response_filter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function assert_not_consumed() {
|
||||
if ($this->consumed) {
|
||||
throw new Error("This FetchingCoroutine has been used up by a transforming call");
|
||||
}
|
||||
}
|
||||
|
||||
private function consume() {
|
||||
$this->assert_not_consumed();
|
||||
$this->consumed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function stop_on_failure(): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$haltable = function () {
|
||||
foreach ($this->generator as $id => $handle) {
|
||||
if (!(yield $id => $handle)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return $this->generator->getReturn();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($haltable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the current coroutine to retry fetches. 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.
|
||||
*/
|
||||
public function retryable(int $retries, bool $tallied_retries = true): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$coroutine = $this;
|
||||
$retryable = function () use ($retries, $coroutine, $tallied_retries) {
|
||||
processing_new_coroutine:
|
||||
while ($coroutine->valid()) {
|
||||
$retries_current = $retries;
|
||||
$id = $coroutine->current_key();
|
||||
$handle = $coroutine->current_request();
|
||||
$attempt_no = 1;
|
||||
do {
|
||||
if (!($attempt_handle = curl_copy_handle($handle))) {
|
||||
log_error("Failed to clone cURL handle");
|
||||
$coroutine->send(false);
|
||||
goto processing_new_coroutine;
|
||||
}
|
||||
|
||||
/** @var CurlHandle|false $response_handle */
|
||||
$response_handle = yield $id => $attempt_handle;
|
||||
$url = curl_getinfo($attempt_handle, CURLINFO_EFFECTIVE_URL);
|
||||
|
||||
if ($response_handle) {
|
||||
$retcode = curl_getinfo($response_handle, CURLINFO_HTTP_CODE);
|
||||
$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;
|
||||
}
|
||||
|
||||
log_debug("Attempt #$attempt_no for $url failed or was rejected upstream.");
|
||||
|
||||
$attempt_no++;
|
||||
} while ($retries_current-- > 0);
|
||||
|
||||
// failed to fetch handle
|
||||
$coroutine->send(false);
|
||||
|
||||
// decrease the remaining retries
|
||||
if ($tallied_retries) {
|
||||
$retries = $retries_current;
|
||||
}
|
||||
}
|
||||
return $coroutine->return_value();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($retryable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the current coroutine to attempt HTTPS->HTTP downgrade after 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.
|
||||
*/
|
||||
public function downgradeable(mixed &$did_downgrade = NULL): \FetchingCoroutine {
|
||||
$this->consume();
|
||||
$coroutine = $this;
|
||||
$has_downgrade_ref = func_num_args() >= 1;
|
||||
if ($has_downgrade_ref) $did_downgrade = false;
|
||||
$downgradeable = function () use ($coroutine, &$did_downgrade, $has_downgrade_ref) {
|
||||
while ($coroutine->valid()) {
|
||||
$id = $coroutine->current_key();
|
||||
$handle = $coroutine->current_request();
|
||||
$handle_downgraded = curl_handle_downgrade($handle);
|
||||
// Try HTTPS first
|
||||
if ($handle_downgraded) {
|
||||
// Skip to next handle on success
|
||||
if ($coroutine->send(yield $id => $handle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($has_downgrade_ref) $did_downgrade = true;
|
||||
$handle = $handle_downgraded;
|
||||
}
|
||||
|
||||
// Use HTTP
|
||||
$coroutine->send(yield $id => $handle);
|
||||
}
|
||||
return $coroutine->return_value();
|
||||
};
|
||||
return $this->project_coroutine_parameters(new FetchingCoroutine($downgradeable()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign non-generator parameters to given FetchingCoroutine.
|
||||
*/
|
||||
private function project_coroutine_parameters(\FetchingCoroutine $coroutine): \FetchingCoroutine {
|
||||
return $coroutine->set_response_filter($this->response_filter);
|
||||
}
|
||||
|
||||
private function is_valid_response(CurlHandle $handle) {
|
||||
$response_filter = $this->response_filter;
|
||||
return $response_filter($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key of the handle yielded at this point in the coroutine, if applicable.
|
||||
*/
|
||||
public function current_key() {
|
||||
return $this->generator->key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cURL handle yielded at this point in the coroutine, if applicable.
|
||||
*/
|
||||
public function current_request(): CurlHandle|null {
|
||||
return $this->generator->current();
|
||||
}
|
||||
|
||||
private function valid(): bool {
|
||||
return $this->generator->valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the current coroutine. Consumes coroutine.
|
||||
* @return \Generator<int,CurlHandle,CurlHandle|false,TReturn>
|
||||
*/
|
||||
public function run() {
|
||||
$this->consume();
|
||||
// passthrough
|
||||
return yield from $this->generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the return value of the wrapped generator object once finished.
|
||||
* @return TReturn
|
||||
*/
|
||||
public function return_value(): mixed {
|
||||
return $this->generator->getReturn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step coroutine until next yield point or end.
|
||||
* Coroutine must not be consumed by any transformations.
|
||||
* @param CurlHandle|false $response
|
||||
* Processed handle corresponding to yielded handle or false in case of failure.
|
||||
*/
|
||||
public function advance(CurlHandle|false $response_handle): bool {
|
||||
$this->assert_not_consumed();
|
||||
return $this->send($response_handle);
|
||||
}
|
||||
|
||||
private function send(CurlHandle|false $handle): bool {
|
||||
if ($handle && $this->is_valid_response($handle)) {
|
||||
$this->generator->send($handle);
|
||||
return true;
|
||||
} else {
|
||||
$this->generator->send(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FetchingCoroutineRunner {
|
||||
/**
|
||||
* Collection of enroled transfers.
|
||||
*/
|
||||
private CurlMultiHandle $transfers;
|
||||
|
||||
/**
|
||||
* Coroutines executed by runner.
|
||||
* @var \FetchingCoroutine[] $coroutines
|
||||
*/
|
||||
private array $coroutines;
|
||||
|
||||
/**
|
||||
* Create new FetchingCoroutineRunner instance with the given coroutines.
|
||||
* @param \FetchingCoroutine[] $coroutines Coroutines to run in parallel.
|
||||
*/
|
||||
public function __construct(array $coroutines = []) {
|
||||
$this->coroutines = $coroutines;
|
||||
|
||||
$this->initialize_coroutines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches all coroutines in parallel.
|
||||
* @return int CURLM_* status.
|
||||
*/
|
||||
public function run_all(): int {
|
||||
do {
|
||||
$curlm_status = curl_multi_exec($this->transfers, $curlm_active_transfer);
|
||||
if ($curlm_active_transfer) {
|
||||
// Block 1 second for pending transfers
|
||||
curl_multi_select($this->transfers, timeout: 1.0);
|
||||
// curl_multi_select($transfers, timeout: 6.0);
|
||||
}
|
||||
$this->process_curl_activity();
|
||||
} while ($curlm_active_transfer && $curlm_status == CURLM_OK);
|
||||
|
||||
return $curlm_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrol initial transfers from all coroutines.
|
||||
*/
|
||||
private function initialize_coroutines() {
|
||||
$this->transfers = curl_multi_init();
|
||||
|
||||
foreach ($this->coroutines as $id => $coroutine) {
|
||||
$this->poll_coroutine_for_transfer($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrol latest transfer from coroutine with given id.
|
||||
*/
|
||||
private function poll_coroutine_for_transfer(int $id) {
|
||||
$coroutine = $this->coroutines[$id];
|
||||
$handle = $coroutine->current_request();
|
||||
if (!$handle) return;
|
||||
curl_setopt($handle, CURLOPT_PRIVATE, $id);
|
||||
curl_multi_add_handle($this->transfers, $handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to new activity on enroled transfers.
|
||||
*/
|
||||
private function process_curl_activity() {
|
||||
while (false !== ($info = curl_multi_info_read($this->transfers))) {
|
||||
if ($info['msg'] != CURLMSG_DONE) continue;
|
||||
/**
|
||||
* @var \CurlHandle $handle
|
||||
*/
|
||||
$handle = $info['handle'];
|
||||
curl_multi_remove_handle($this->transfers, $handle);
|
||||
$coroutine_id = curl_getinfo($handle, CURLINFO_PRIVATE);
|
||||
if (!isset($this->coroutines[$coroutine_id])) {
|
||||
throw new Error("Invalid coroutine ID: " + $coroutine_id);
|
||||
}
|
||||
$this->coroutines[$coroutine_id]->advance($handle);
|
||||
$this->poll_coroutine_for_transfer($coroutine_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
@ -1,127 +0,0 @@
|
||||
<?php
|
||||
require_once 'servers/known-servers.php';
|
||||
|
||||
/**
|
||||
* Return local path to room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
*/
|
||||
function room_icon_path(string $room_id): string {
|
||||
global $ROOM_ICONS_CACHE;
|
||||
return "$ROOM_ICONS_CACHE/$room_id";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return local path to resized room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
* @param string $size Image dimensions.
|
||||
*/
|
||||
function room_icon_path_resized(string $room_id, string $size): string {
|
||||
global $ROOM_ICONS;
|
||||
return "$ROOM_ICONS/$room_id-$size";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return server path to room icon.
|
||||
* @param string $room_id Id of room to locate icon for.
|
||||
* @param string $size Image dimensions.
|
||||
*/
|
||||
function room_icon_path_relative(string $room_id, string $size): string {
|
||||
global $ROOM_ICONS_RELATIVE;
|
||||
return "$ROOM_ICONS_RELATIVE/$room_id-$size";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<int,CurlHandle,CurlHandle|false,void>
|
||||
*/
|
||||
function fetch_room_icon_coroutine(\CommunityRoom $room): Generator {
|
||||
if (room_icon_safety($room) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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");
|
||||
|
||||
// Re-fetch icons periodically.
|
||||
if (!file_exists($icon_cached) || $icon_expired) {
|
||||
$icon_url = $room->get_icon_url();
|
||||
if (empty($icon_url)) {
|
||||
return null;
|
||||
}
|
||||
log_debug("Fetching icon for $room_id.");
|
||||
$icon_response = yield from FetchingCoroutine::from_url($icon_url)->run();
|
||||
$icon = $icon_response ? curl_multi_getcontent($icon_response) : null;
|
||||
if (empty($icon)) {
|
||||
log_info("$room_id returned an empty icon.");
|
||||
}
|
||||
// Never overwrite with an empty file.
|
||||
if (!(file_exists($icon_cached) && filesize($icon_cached) > 0 && empty($icon))) {
|
||||
file_put_contents($icon_cached, $icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the 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 {
|
||||
list($width, $height) = explode("x", $size);
|
||||
$width = intval($width);
|
||||
$height = intval($height);
|
||||
assert(!empty($width) && !empty($height));
|
||||
|
||||
if (room_icon_safety($room) < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$room_id = $room->get_room_identifier();
|
||||
$icon_cached = room_icon_path($room_id);
|
||||
$icon_resized = room_icon_path_resized($room_id, $size);
|
||||
$icon_expired = file_exists($icon_cached) && filemtime($icon_cached) < strtotime("-1 day");
|
||||
|
||||
if (!file_exists($icon_cached)) {
|
||||
log_debug("Missing icon asset for $room_id");
|
||||
return "";
|
||||
}
|
||||
if (!file_exists($icon_resized) || $icon_expired) {
|
||||
$icon_cached_contents = file_get_contents($icon_cached);
|
||||
if (empty($icon_cached_contents)) {
|
||||
file_put_contents($icon_resized, "");
|
||||
return "";
|
||||
}
|
||||
// Resize image
|
||||
$gd_image = imagecreatefromstring($icon_cached_contents);
|
||||
$gd_resized = imagescale($gd_image, $width, $height);
|
||||
if (!imagewebp($gd_resized, $icon_resized)) {
|
||||
log_info("Converting image for $room_id to $size failed");
|
||||
}
|
||||
}
|
||||
if (filesize($icon_resized) == 0) {
|
||||
return "";
|
||||
}
|
||||
return room_icon_path_relative($room_id, $size);
|
||||
}
|
||||
|
||||
function room_icon_safety(\CommunityRoom $room): int {
|
||||
global $ICON_ALLOWLIST, $ICON_BLOCKLIST;
|
||||
if (in_array($room->get_room_identifier(), $ICON_BLOCKLIST)) {
|
||||
return -1;
|
||||
}
|
||||
if (in_array($room->server->get_hostname(), $ICON_ALLOWLIST)) {
|
||||
return 1;
|
||||
}
|
||||
if (in_array($room->server->get_hostname(), $ICON_BLOCKLIST)) {
|
||||
return -1;
|
||||
}
|
||||
if ($room->has_nsfw_keywords()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
file_exists($ROOM_ICONS_CACHE) or mkdir($ROOM_ICONS_CACHE, 0755, true);
|
||||
file_exists($ROOM_ICONS) or mkdir($ROOM_ICONS, 0755, true);
|
||||
?>
|
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
require_once 'servers/known-servers.php';
|
||||
require_once 'utils/room-icons.php';
|
||||
|
||||
/**
|
||||
* Fetch the icon of the given Community server and return its relative path.
|
||||
* @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 {
|
||||
global $SERVER_ICON_MAPPING;
|
||||
$hostname = $server->get_hostname();
|
||||
if (!isset($SERVER_ICON_MAPPING[$hostname])) {
|
||||
return "";
|
||||
}
|
||||
$room_token = $SERVER_ICON_MAPPING[$hostname];
|
||||
$room = $server->get_room_by_token($room_token);
|
||||
if (!$room) {
|
||||
log_warning("Room $room_token on $hostname does not exist, cannot be used as icon.");
|
||||
return "";
|
||||
}
|
||||
return room_icon($room, $size);
|
||||
}
|
||||
?>
|
@ -1,318 +0,0 @@
|
||||
<?php
|
||||
require_once 'utils.php';
|
||||
require_once 'tags.php';
|
||||
|
||||
class SDIRCommunitySource {
|
||||
private function __construct(string $contents) {
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance of this source from contents.
|
||||
* Returns false if processing the source fails.
|
||||
* @return \SDIRCommunitySource|false
|
||||
*/
|
||||
public static function from_contents(string $contents) {
|
||||
$source = new SDIRCommunitySource($contents);
|
||||
|
||||
if (!$source->sdir_process_tags()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private readonly string $contents;
|
||||
|
||||
/**
|
||||
* @var string[][] $tags Array associating room IDs with string tag arrays.
|
||||
*/
|
||||
private array $tags;
|
||||
|
||||
private static function sdir_validate_entry(
|
||||
array $room_entry,
|
||||
bool &$missing_url,
|
||||
bool &$missing_tags
|
||||
): bool {
|
||||
if (!isset($room_entry['url']) || !is_string($room_entry['url'])) {
|
||||
log_value($room_entry);
|
||||
$missing_url = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($room_entry['tags']) || !is_string($room_entry['tags'])) {
|
||||
log_value($room_entry);
|
||||
$missing_tags = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function sdir_report_errors(bool $entry_missing_url, bool $entry_missing_tags) {
|
||||
if ($entry_missing_url) {
|
||||
log_error("One or more room entries from session.directory is missing the 'url' parameter.");
|
||||
}
|
||||
|
||||
if ($entry_missing_tags) {
|
||||
log_error("One or more room entries from session.directory is missing the 'tags' parameter.");
|
||||
}
|
||||
}
|
||||
|
||||
private function get_sdir_entries(): array|bool {
|
||||
try {
|
||||
return json_decode($this->contents, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function sdir_process_tags(): bool {
|
||||
$entry_missing_url = false;
|
||||
$entry_missing_tags = false;
|
||||
|
||||
$rooms = SDIRCommunitySource::get_sdir_entries($this->contents);
|
||||
|
||||
if (!$rooms) {
|
||||
log_error("Could not parse entries from session.directory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rooms as $room_entry) {
|
||||
if (!SDIRCommunitySource::sdir_validate_entry(
|
||||
$room_entry, $entry_missing_url, $entry_missing_tags
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$url = $room_entry['url'];
|
||||
$tags = $room_entry['tags'];
|
||||
|
||||
$room_id = url_get_room_id($url);
|
||||
|
||||
$this->tags[$room_id] = explode(',', $tags);
|
||||
}
|
||||
|
||||
SDIRCommunitySource::sdir_report_errors($entry_missing_url, $entry_missing_tags);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][] Array associating room IDs with string tag arrays.
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
||||
|
||||
class ASGLCommunitySource {
|
||||
private function __construct(string $contents) {
|
||||
$this->contents = $contents;
|
||||
}
|
||||
|
||||
private readonly string $contents;
|
||||
|
||||
/**
|
||||
* @var string[][] $tags;
|
||||
*/
|
||||
private array $tags = [];
|
||||
|
||||
/**
|
||||
* @return \ASGLCommunitySource|false
|
||||
*/
|
||||
public static function from_contents(string $contents) {
|
||||
$source = new ASGLCommunitySource($contents);
|
||||
|
||||
if(!$source->asgl_process_tags()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private function asgl_process_tags(): bool {
|
||||
$lines = explode("\n", $this->contents);
|
||||
// $last_headings = [];
|
||||
$last_room_id = null;
|
||||
$room_tags = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
ASGLCommunitySource::asgl_process_line($line, $last_room_id, $room_tags);
|
||||
|
||||
if ($last_room_id != null && count($room_tags) > 0) {
|
||||
$this->tags[$last_room_id] = $room_tags;
|
||||
$last_room_id = null;
|
||||
$room_tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function asgl_process_line(
|
||||
?string $line,
|
||||
?string &$last_room_id,
|
||||
array &$room_tags
|
||||
) {
|
||||
$line = trim($line);
|
||||
|
||||
if (strlen($line) == 0) {
|
||||
$last_room_id = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$urls = parse_join_links($line);
|
||||
|
||||
if (count($urls) == 1 && $urls[0] == $line) {
|
||||
$last_room_id = url_get_room_id($urls[0]);
|
||||
$room_tags = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (str_starts_with($line, "hashtag")) {
|
||||
$room_tags = ASGLCommunitySource::read_asgl_tags($line);
|
||||
}
|
||||
}
|
||||
|
||||
private static function read_asgl_tags(string $line): array {
|
||||
$matches = [];
|
||||
preg_match_all('/`#([^`]+)`/', $line, $matches);
|
||||
// Return first group matches.
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
public function get_tags(): array {
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
||||
|
||||
class CommunitySources {
|
||||
private const SOURCES = array(
|
||||
'ASGL' => 'https://raw.githubusercontent.com/GNU-Linux-libre/Awesome-Session-Group-List/main/README.md',
|
||||
'LOKI' => 'https://lokilocker.com/Mods/Session-Groups/wiki/Session-Open-Groups',
|
||||
'SDIR' => 'https://session.directory/?all=groups',
|
||||
'SDIR-BASE' => 'https://session.directory/',
|
||||
'SDIR-PATTERN' => '/view_session_group_user_lokinet\.php\?id=\d+/',
|
||||
'SDIR-JSON' => 'https://session.directory/scrape.php',
|
||||
'FARK' => 'https://freearkham.cc/'
|
||||
);
|
||||
|
||||
private readonly string $contents_asgl;
|
||||
private readonly string $contents_loki;
|
||||
private readonly string $contents_sdir;
|
||||
private readonly string $contents_fark;
|
||||
private readonly string $contents_aggregated;
|
||||
|
||||
/**
|
||||
* Arraying associating room identifiers with arrays of raw tags.
|
||||
* @var array<string,string[]> $room_tags
|
||||
*/
|
||||
private array $room_tags = [];
|
||||
|
||||
/**
|
||||
* Fetches and saves known sources of Session Community join links.
|
||||
*/
|
||||
public function __construct() {
|
||||
log_info("Requesting Awesome Session Group list...");
|
||||
$this->contents_asgl = CommunitySources::fetch_source('ASGL');
|
||||
|
||||
log_info("Requesting Lokilocker Mods Open Group list...");
|
||||
$this->contents_loki = CommunitySources::fetch_source('LOKI');
|
||||
|
||||
log_info("Requesting session.directory list...");
|
||||
$this->contents_sdir = CommunitySources::fetch_source('SDIR-JSON');
|
||||
|
||||
log_info("Requesting FreeArkham.cc list...");
|
||||
$this->contents_fark = CommunitySources::fetch_source('FARK');
|
||||
|
||||
log_info("Parsing extra information...");
|
||||
if (!$this->process_sources()) {
|
||||
log_error("Could not parse extra information from one or more sources.");
|
||||
}
|
||||
|
||||
log_info('Done fetching sources.');
|
||||
|
||||
$this->contents_aggregated =
|
||||
$this->contents_asgl .
|
||||
$this->contents_fark .
|
||||
$this->contents_loki .
|
||||
// Slashes are escaped when served, unescape them
|
||||
str_replace("\\/", "/", $this->contents_sdir);
|
||||
}
|
||||
|
||||
private static function fetch_source(string $source_key) {
|
||||
$url = CommunitySources::SOURCES[$source_key];
|
||||
|
||||
$contents = file_get_contents($url);
|
||||
log_debug($http_response_header[0]);
|
||||
|
||||
if (!$contents) {
|
||||
log_error("Could not fetch source from $url.");
|
||||
return "";
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[][] $tags Array associating room IDs to tag arrays
|
||||
*/
|
||||
private function add_tags(array $tags) {
|
||||
foreach ($tags as $room_id => $room_tags) {
|
||||
if (!isset($this->room_tags[$room_id])) {
|
||||
$this->room_tags[$room_id] = [];
|
||||
}
|
||||
|
||||
$this->room_tags[$room_id] = [
|
||||
...$this->room_tags[$room_id],
|
||||
...$room_tags
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function process_sources(): bool {
|
||||
$source_sdir = SDIRCommunitySource::from_contents($this->contents_sdir);
|
||||
|
||||
$source_asgl = ASGLCommunitySource::from_contents($this->contents_asgl);
|
||||
|
||||
$source_sdir && $this->add_tags($source_sdir->get_tags());
|
||||
|
||||
$source_asgl && $this->add_tags($source_asgl->get_tags());
|
||||
|
||||
if (!$source_sdir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$source_asgl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all join URLs found.
|
||||
* @return string[] Join URLs.
|
||||
*/
|
||||
public function get_join_urls(): array {
|
||||
return array_unique(
|
||||
parse_join_links($this->contents_aggregated)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return known tags for the given room.
|
||||
* @param string $room_id Room identifier.
|
||||
* @return \CommunityTag[] Array of string tags.
|
||||
*/
|
||||
public function get_room_tags($room_id): array {
|
||||
if (!isset($this->room_tags[$room_id])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->room_tags[$room_id];
|
||||
}
|
||||
}
|
||||
?>
|
@ -1,185 +0,0 @@
|
||||
<?php
|
||||
require_once 'utils.php';
|
||||
|
||||
class TagType {
|
||||
private function __construct() {}
|
||||
const USER_TAG = 0;
|
||||
const RESERVED_TAG = 1;
|
||||
const WARNING_TAG = 2;
|
||||
}
|
||||
|
||||
class CommunityTag implements JsonSerializable {
|
||||
public function __construct(
|
||||
string $text,
|
||||
int $tag_type = TagType::USER_TAG,
|
||||
string $description = ""
|
||||
) {
|
||||
$this->text = $text;
|
||||
$this->type = $tag_type;
|
||||
$this->description =
|
||||
empty($description) ? "Tag: $text" : $description;
|
||||
}
|
||||
|
||||
public readonly int $type;
|
||||
|
||||
public readonly string $text;
|
||||
|
||||
public readonly string $description;
|
||||
|
||||
/**
|
||||
* Returns 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.
|
||||
*/
|
||||
public function get_text(): string {
|
||||
return strtolower($this->text);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): mixed {
|
||||
// Only used for passing to DOM
|
||||
$details = get_object_vars($this);
|
||||
$details['text'] = html_sanitize($this->get_text());
|
||||
$details['description'] = html_sanitize($details['description']);
|
||||
$details['type'] = $this->get_tag_type();
|
||||
return $details;
|
||||
}
|
||||
|
||||
private static function preprocess_tag(?string $tag) {
|
||||
$tag = trim($tag);
|
||||
|
||||
if (strlen($tag) == 0) {
|
||||
return $tag;
|
||||
}
|
||||
|
||||
$tag = html_sanitize(html_entity_decode($tag));
|
||||
|
||||
if ($tag[0] == '#') {
|
||||
return substr($tag, 1);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $tag_array
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
private static function from_tag_array(array $tag_array) {
|
||||
$tags = array_map(function(?string $tag) {
|
||||
return CommunityTag::preprocess_tag($tag);
|
||||
}, $tag_array);
|
||||
|
||||
$tags = array_filter(
|
||||
$tags, function(?string $tag) {
|
||||
return strlen($tag) != 0;
|
||||
}
|
||||
);
|
||||
|
||||
return array_map(function(string $tag) {
|
||||
return new CommunityTag($tag);
|
||||
}, $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user tags given, without any reserved tags.
|
||||
* @param string[] $tags
|
||||
* @param bool $remove_redundant Removes duplicate and obvious tags.
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function from_user_tags(
|
||||
array $tags, bool $remove_redundant = false
|
||||
): array {
|
||||
$tags_user = array_filter(
|
||||
$tags,
|
||||
function($tag) {
|
||||
return !CommunityTag::is_reserved_tag($tag);
|
||||
}
|
||||
);
|
||||
|
||||
$tags_built = CommunityTag::from_tag_array($tags_user);
|
||||
|
||||
if ($remove_redundant) {
|
||||
$tags_built = CommunityTag::dedupe_tags($tags_built);
|
||||
$tags_built = array_filter($tags_built, function(\CommunityTag $tag) {
|
||||
$text = strtolower($tag->text);
|
||||
return !in_array($text, CommunityTag::REDUNDANT_TAGS);
|
||||
});
|
||||
}
|
||||
|
||||
return $tags_built;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $details_array Array of string tags.
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function from_details_array(array $details_array): array {
|
||||
return CommunityTag::from_user_tags($details_array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \CommunityTag[] $tags
|
||||
* @return \CommunityTag[]
|
||||
*/
|
||||
public static function dedupe_tags(array $tags) {
|
||||
return array_unique($tags);
|
||||
}
|
||||
|
||||
public function get_tag_classname(): string {
|
||||
$tag_type = $this->get_tag_type();
|
||||
$classname = "room-label-$tag_type";
|
||||
if (CommunityTag::is_showcased_tag($this->text)) {
|
||||
$classname .= " room-label-showcased";
|
||||
}
|
||||
return $classname;
|
||||
}
|
||||
|
||||
public function get_tag_type(): string {
|
||||
return match($this->type) {
|
||||
TagType::USER_TAG => 'user',
|
||||
TagType::RESERVED_TAG => 'reserved',
|
||||
TagType::WARNING_TAG => 'warning'
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @var string[] RESERVED_TAGS
|
||||
* Array of derived tags unavailable for manual tagging.
|
||||
*/
|
||||
private const RESERVED_TAGS = [
|
||||
"official",
|
||||
"nsfw",
|
||||
"new",
|
||||
"modded",
|
||||
"not modded",
|
||||
"read-only",
|
||||
"uploads off",
|
||||
"we're here"
|
||||
];
|
||||
|
||||
private const SHOWCASED_TAGS = ["official", "new", "we're here"];
|
||||
|
||||
private const REDUNDANT_TAGS = ["session"];
|
||||
|
||||
public const NSFW_KEYWORDS = ["nsfw", "porn", "erotic", "18+"];
|
||||
|
||||
public const CHECK_MARK = "✅";
|
||||
|
||||
public const WARNING_ICON = "⚠️";
|
||||
|
||||
/**
|
||||
* Checks whether the given manual tag can be accepted.
|
||||
*/
|
||||
public static function is_reserved_tag(string $tag): bool {
|
||||
return in_array(strtolower($tag), CommunityTag::RESERVED_TAGS);
|
||||
}
|
||||
|
||||
public static function is_showcased_tag(string $tag): bool {
|
||||
return in_array(strtolower($tag), CommunityTag::SHOWCASED_TAGS);
|
||||
}
|
||||
}
|
||||
?>
|
@ -1,31 +1,32 @@
|
||||
<?php
|
||||
include_once "$PROJECT_ROOT/php/utils/room-icons.php";
|
||||
require_once 'php/assets/room-icons.php';
|
||||
|
||||
/**
|
||||
* @var \CommunityRoom[] $rooms
|
||||
*/
|
||||
/**
|
||||
* @var \CommunityRoom[] $rooms
|
||||
*/
|
||||
|
||||
$json_ld_data = array(
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"summary" => "Active Session Communities",
|
||||
"type" => "Collection",
|
||||
"totalItems" => count($rooms),
|
||||
"items" => array_map(function(\CommunityRoom $room) {
|
||||
$values = array(
|
||||
"type" => "Group",
|
||||
"name" => $room->name,
|
||||
"summary" => $room->description,
|
||||
"startTime" => $room->created ? date('Y-m-d\TH:i:s', intval($room->created)) : null,
|
||||
"url" => $room->get_preview_url(),
|
||||
"icon" => room_icon($room, '64x64')
|
||||
);
|
||||
return array_filter($values, function ($value) {
|
||||
return $value != null;
|
||||
});
|
||||
}, $rooms)
|
||||
);
|
||||
$json_ld_data = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'summary' => 'Active Session Communities',
|
||||
'type' => 'Collection',
|
||||
'totalItems' => count($rooms),
|
||||
'items' => array_map(function(\CommunityRoom $room) {
|
||||
$values = array(
|
||||
'type' => 'Group',
|
||||
'name' => $room->name,
|
||||
'summary' => $room->description,
|
||||
'startTime' => $room->created ? date('Y-m-d\TH:i:s', intval($room->created)) : null,
|
||||
'url' => $room->get_preview_url(),
|
||||
'icon' => room_icon($room, '64x64')
|
||||
);
|
||||
return array_filter($values, function ($value) {
|
||||
return $value != null;
|
||||
});
|
||||
}, $rooms)
|
||||
);
|
||||
?>
|
||||
|
||||
<script type="application/ld+json">
|
||||
<?=json_encode($json_ld_data)?>
|
||||
</script>
|
||||
<?=json_encode($json_ld_data)?>
|
||||
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?php
|
||||
require_once "$PROJECT_ROOT/php/utils/room-invites.php";
|
||||
require_once 'php/assets/room-invites.php';
|
||||
?>
|
||||
<dialog id="details-modal">
|
||||
<div id="details-modal-contents">
|
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
require_once "$PROJECT_ROOT/php/utils/utils.php";
|
||||
require_once "$PROJECT_ROOT/php/utils/servers-rooms.php";
|
||||
require_once "$PROJECT_ROOT/php/utils/room-invites.php";
|
||||
require_once "$PROJECT_ROOT/php/utils/room-icons.php";
|
||||
require_once "$PROJECT_ROOT/php/utils/server-icons.php";
|
||||
require_once 'php/utils/utils.php';
|
||||
require_once 'php/servers/servers-rooms.php';
|
||||
require_once 'php/assets/room-invites.php';
|
||||
require_once 'php/assets/room-icons.php';
|
||||
require_once 'php/assets/server-icons.php';
|
||||
|
||||
/**
|
||||
* @var CommunityRoom[] $rooms
|
Loading…
Reference in New Issue