Reorganizing & codestyle compliance

dev
gravel 12 months ago
parent 6fc91007a4
commit ceea186ded
Signed by: gravel
GPG Key ID: C0538F3C906B308F

@ -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

@ -33,8 +33,9 @@ Recommended:
- Add the [default include paths](.phpenv) (`.`, `php`) to your PHP intellisense.
- Symlink the commit hooks provided in [`etc/hooks`](etc/hooks/) to `.git/hooks/<hook>` to run a full test cycle when committing to main.
- Get [EditorConfig](https://editorconfig.org/#pre-installed) for your IDE if not pre-installed.
- Symlink the commit hooks provided in [`etc/hooks`](etc/hooks/) to `.git/hooks/<hook>` to run a full test cycle when committing to main.
### Running your own copy
@ -55,6 +56,33 @@ Recommended:
**Comments and documentation**: [PHPDoc](https://en.wikipedia.org/wiki/PHPDoc)
**Whitespace**:
- The following exceptions apply to PHP expressions embedded within HTML:
- Flow control statements within HTML (`<?php if ($condition): >`)
shall have zero indentation, akin to C macros or sh heredocs.
- Self-contained PHP `include` and variable shorthand statements
in multi-line HTML child node position shall be followed by an extra line:
```php
<body>
<div>
<?php if ($bowl->has_food()): >
<?= $bowl->describe_food() ?>
<?php else: >
<?php include 'bowl-empty.php'; >
<?php endif; >
</div>
</body>
```
**Other**:
- Strings shall be surrounded by single quotes `''`
where no variable expansion is taking place
### HTML & CSS
**Identifier casing**: `kebab-case`, legacy `snake_case`

@ -26,6 +26,6 @@ Language labels for communities are grouped by server for easy navigation:
);
```
To label a Community, you would search for the `xxxx` suffix (in this case `a03c`) in the [flags file](./language_flags.php) and add the appropriate entry.
To label a Community, you would search for the `xxxx` suffix (in this case `a03c`) in the [flags file](./language-flags.php) and add the appropriate entry.
If you cannot any labels for this code, copy an existing block from another server & replace the entries with your own. If you have trouble entering language flags on your keyboard, you may find it helpful to copy them from <https://www.alt-codes.net/flags>.

@ -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);
}
?>

@ -5,11 +5,9 @@
require_once 'utils/getopt.php';
require_once 'utils/utils.php';
require_once 'servers/known-servers.php';
require_once 'utils/servers-rooms.php';
require_once 'utils/sources.php';
// Not required
include_once "$LANGUAGES_ROOT/language_flags.php";
require_once 'servers/servers-rooms.php';
require_once 'servers/sources.php';
require_once 'languages/language-flags.php';
/**
* Fetch online Communities and write the resulting data to disk.

@ -1,7 +1,7 @@
<?php
require_once "getenv.php";
require_once "php/utils/servers-rooms.php";
require_once "php/utils/logging.php";
require_once "utils/logging.php";
require_once "servers/servers-rooms.php";
class CommunityListing implements JsonSerializable {
public readonly string $id;

@ -1,10 +1,10 @@
<?php
require_once 'languages/language_flags.php';
require_once 'languages/language-flags.php';
require_once 'servers/known-servers.php';
require_once 'tags.php';
require_once 'fetching-coroutines.php';
require_once 'room-icons.php';
require_once 'room-invites.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;

@ -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,5 +1,5 @@
<?php
require_once 'utils.php';
require_once 'utils/utils.php';
/**
* @template TReturn

@ -1,5 +1,5 @@
<?php
include_once 'logging.php';
include_once 'utils/logging.php';
// Read the -v|--verbose option increasing logging verbosity to debug.
$options = getopt("vn", ["verbose", "fast", "no-color", "dry-run"]);

@ -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,23 +1,23 @@
<?php
include_once "$PROJECT_ROOT/php/utils/room-icons.php";
require_once 'php/assets/room-icons.php';
/**
* @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) {
'@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')
'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;
@ -28,4 +28,5 @@
<script type="application/ld+json">
<?=json_encode($json_ld_data)?>
</script>

@ -5,7 +5,9 @@
<meta
http-equiv="Content-Security-Policy"
content="
script-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'none';
object-src 'none'; media-src 'none'; form-action 'none'; base-uri 'self';
script-src 'self'; img-src 'self' data:;
connect-src 'self'; font-src 'none';
object-src 'none'; media-src 'none';
form-action 'none'; base-uri 'self';
"
>

@ -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

@ -1,9 +1,9 @@
<?php
// prerequisite include for sites and components
require_once '+getenv.php';
require_once 'php/utils/utils.php';
require_once 'php/utils/servers-rooms.php';
require_once 'php/utils/getopt.php';
require_once 'php/utils/utils.php';
require_once 'php/servers/servers-rooms.php';
// Read the server data from disk.
$servers_raw = file_get_contents($ROOMS_FILE);
@ -55,6 +55,7 @@
<meta property="og:locale" content="en_US"/>
<meta name="timestamp" content="<?=$timestamp?>">
<?php include "+components/communities-json-ld.php"; ?>
</head>
<body>
<input type="checkbox" id="toggle-theme-switch">
@ -79,9 +80,9 @@
</div>
</header>
<h1 id="headline">Session Communities</h1>
<?php include "+components/qr_modals.php" ?>
<?php include "+components/qr-modals.php" ?>
<?php include "+components/tbl_communities.php" ?>
<?php include "+components/tbl-communities.php" ?>
<hr>

@ -1,5 +1,5 @@
<?php
include_once "+getenv.php";
require_once '+getenv.php';
$instruction_files = glob("+instructions/*.txt");
function file_language($file) { return pathinfo($file)['filename']; }
?>
@ -62,7 +62,6 @@
</label>
<?php endforeach; ?>
<article id="instructions">
<?php foreach ($instruction_files as $i => $file): ?>
<section id="instructions-<?=$i?>" class="instructions"><?php

Loading…
Cancel
Save