1
0
Fork 1

Merge branch 'main' into querying-refactor

pull/26/head
gravel 1 year ago
commit 8ae94afa80
Signed by: gravel
SSH Key Fingerprint: SHA256:p4HP49CCk4YQMkJpWJ09L8peEPQWjERtdCRAFxPfbOY

@ -0,0 +1,65 @@
# Contribution guidelines
## Development environment
### Prerequisites
- PHP (version TBD)
- `make`
- `entr` to watch for file changes
- `xdg-open` link handler to invoke browser
- patience
### Cloning or updating the repository
Ensure the consistency of the `languages` submodule by using the following options:
- `git clone --recurse-submodules <repository-url>`
- `git pull --recurse-submodules`
### Development
Run at least once: `make fetch` to query servers. This can take around 5 minutes.
Run when developing: `make dev` to watch for changes & serve HTML locally in browser.
Does not respond to new files.
See [`Makefile`](Makefile) for more details.
### Running your own copy
- point your webserver at the `output` folder
- install systemd services from the `systemd` folder or an equivalent timer
- `session_sudoers`: TBD
## Code style guidelines
### General
**Indentation**: Tabs (4-wide)
**Filename seperator**: Hyphen (`-`)
### PHP
**Identifier casing**: `snake_case` and `CONSTANT_CASE`
**Comments and documentation**: TBD
### HTML & CSS
**Identifier casing**: `kebab-case`, occasional `snake_case`
**Comments and documentation**: TBD
### JavaScript
**Identifier casing**: `camelCase` and `CONSTANT_CASE`, occasional `snake_case`
**Comments and documentation**: [JSDoc](https://jsdoc.app/)
## Contact
- Web Development Session Community on [caliban.org](https://sog.caliban.org/)
- Project lead, querying logic, deployment, community filtering: `someguy` on Session
- Documentation, code quality, HTML generation, CSS, JS: `gravel` on Session

@ -2,17 +2,17 @@
## What does this site do?
This script crawls known sources of published Session Communities,
queries their servers for available information and
displays this information as a static HTML page.
This script crawls known sources of published Session Communities,
queries their servers for available information and
displays this information as a static HTML page.
The results of this can be viewed on https://sessioncommunities.online/.
## What is Session?
Session is a private messaging app that protects your meta-data,
encrypts your communications, and makes sure your messaging activities
leave no digital trail behind.
Session is a private messaging app that protects your meta-data,
encrypts your communications, and makes sure your messaging activities
leave no digital trail behind.
https://getsession.org/
@ -22,58 +22,54 @@ https://getsession.org/
Currently this script crawls the following sites:
- https://github.com/GNU-Linux-libre/Awesome-Session-Group-List
- https://lokilocker.com/Mods/Session-Groups/wiki/Session-Open-Groups
- https://session.directory/
- <https://github.com/GNU-Linux-libre/Awesome-Session-Group-List>
- <https://lokilocker.com/Mods/Session-Groups/wiki/Session-Open-Groups>
- <https://session.directory/>
Additionally, the following open community servers are polled:
Additionally, a few other servers are hardcoded, see [querying logic](php/fetch-servers.php).
- https://open.getsession.org
- http://13.233.251.36:8081
### How does this work?
### Steps
The [`update-listing.php`](php/update-listing.php) script invokes the following two PHP scripts: [`fetch-servers.php`](php/fetch-servers.php) to query available servers, and [`generate-html.php`](php/generate-html.php>) to generate the static HTML.
The querying logic consists of these steps:
1. Fetching source HTML: `get_html_from_known_sources()`
1. Extracting Session invites from the HTML:
`extract_join_links_from_html()` and `get_servers_from_join_links()`
1. Making sure servers are online: `reduce_servers()`
1. Querying the servers for all available rooms
and normalize active user numbers: `query_servers_for_rooms()`
1. De-duplicating servers based on public keys:
1. Querying the servers for all available rooms
and normalizing active user numbers: `query_servers_for_rooms()`
1. De-duplicating servers based on public keys:
`get_pubkeys_of_servers()` and `reduce_addresses_of_pubkeys()`
1. Aggregating all server info & adding language data: `generate_info_arrays()`
1. Generating static HTML content: `generateHTML()`
### Legacy support
Right now we fully support legacy SOGS servers,
although this support is likely going to be dropped soon,
since those servers can not even be joined anymore with current Session clients.
Dropping legacy support will also increase maintainability.
Static HTML is generated from the [`sites`](sites) directory to the [`output`](output) directory, which additionally contains static assets. All contents of `sites` are invoked to produce a HTML page unless they are prefixed with a `+` sign.
### Work around bad routing to Chinese servers
Depending on your location, it is possible for you to get really bad routing to
SOGS servers behind the GFW. In this case,
the initial connection is still successful, but you'll never receive
any actual content and the retrieval attempt will simply time out.
Depending on your location, it is possible for you to get really bad routing to
SOGS servers behind the [GFW](https://en.wikipedia.org/wiki/Great_Firewall). In this case,
the initial connection is still successful, but you'll never receive
any actual content and the retrieval attempt will simply time out.
This happens randomly. To make sure this won't affect the results, we simply
check whether the server is online (the initial connection being successful),
and then retry a lot of times with a short timeout
until we eventually get the content.
check whether the server is online (the initial connection being successful),
and then retry a lot of times with a short timeout
until we eventually get the content.
The details can be seen in `curl_get_contents()`.
### Official repositories
- https://github.com/mdPlusPlus/sessioncommunities.online
- https://lokilocker.com/SomeGuy/sessioncommunities.online
If your favourite Session community is missing a language flag,
- GitHub: <https://github.com/mdPlusPlus/sessioncommunities.online>
- Lokinet Gitea: <https://lokilocker.com/SomeGuy/sessioncommunities.online>
If your favourite Session community is missing a language flag,
you can issue a pull request here:
- https://github.com/mdPlusPlus/sessioncommunities.online-languages/
- <https://github.com/mdPlusPlus/sessioncommunities.online-languages/>
## Contact
If you want to contact me, you can add me on Session via my
[ONS](https://docs.oxen.io/using-the-oxen-blockchain/using-oxen-name-system):
"someguy" (without the quotes)
If you want to contact me, you can add me on Session via my
[ONS](https://docs.oxen.io/using-the-oxen-blockchain/using-oxen-name-system):
`someguy`.

@ -1 +1 @@
Subproject commit 524ad983610b096ed09e721c20cba011dfbc7992
Subproject commit f2aaed46ea2ef3a6ff03b6eee6f70c81cc20f069

@ -3,6 +3,7 @@
// change in the foreseeable future.
export const dom = {
/** @return {HTMLTableElement | null} */
tbl_communities: () => document.getElementById("tbl_communities"),
last_checked: () => document.getElementById("last_checked_value"),
qr_modal: (communityID) => document.getElementById(`modal_${communityID}`),
@ -14,7 +15,7 @@ export const dom = {
export const COLUMN = {
IDENTIFIER: 0, LANGUAGE: 1, NAME: 2,
DESCRIPTION: 3, USERS: 4, PREVIEW: 5,
QR_CODE: 6, JOIN_URL: 7
QR_CODE: 6, SERVER_ICON: 7, JOIN_URL: 8
};
// Reverse enum.
@ -33,7 +34,7 @@ export const ATTRIBUTES = {
ACTIVE: 'data-sort',
ASCENDING: 'data-sort-asc',
COLUMN: 'data-sorted-by',
COLUMN_LITERAL: 'sorted-by'
// COLUMN_LITERAL: 'sorted-by'
}
};
@ -41,20 +42,33 @@ export function columnAscendingByDefault(column) {
return column != COLUMN.USERS;
}
export function columnIsSortable(column) { return column != COLUMN.QR_CODE; }
export function columnNeedsCasefold(column) {
return [
COLUMN.IDENTIFIER,
COLUMN.NAME,
COLUMN.DESCRIPTION
export function columnIsSortable(column) {
return ![
COLUMN.QR_CODE,
COLUMN.PREVIEW,
// Join URL contents are not guaranteed to have visible text.
COLUMN.JOIN_URL
].includes(column);
}
export function columnIsNumeric(column) {
return [
COLUMN.USERS
].includes(column);
/**
* @type {Record<string, (el: HTMLTableCellElement) => any>}
*/
const TRANSFORMATION = {
numeric: (el) => parseInt(el.innerText),
casefold: (el) => el.innerText.toLowerCase().trim(),
tokenData: (el) => el.getAttribute("data-token")
}
/**
* @type {Dictionary<number, (el: HTMLTableCellElement) => any>}
*/
export const COLUMN_TRANSFORMATION = {
[COLUMN.USERS]: TRANSFORMATION.numeric,
[COLUMN.IDENTIFIER]: TRANSFORMATION.casefold,
[COLUMN.NAME]: TRANSFORMATION.casefold,
[COLUMN.DESCRIPTION]: TRANSFORMATION.casefold,
[COLUMN.SERVER_ICON]: TRANSFORMATION.tokenData
}
/**
@ -64,22 +78,22 @@ export function columnIsNumeric(column) {
* @returns {HTMLElement}
*/
function createElement(tag, ...args) {
const element = document.createElement(tag);
if (args.length === 0) return element;
const propsCandidate = args[0];
if (typeof propsCandidate !== "string" && !(propsCandidate instanceof Element)) {
// args[0] is not child element or text node
// must be props object
Object.assign(element, propsCandidate);
args.shift();
}
element.append(...args);
return element;
const element = document.createElement(tag);
if (args.length === 0) return element;
const propsCandidate = args[0];
if (typeof propsCandidate !== "string" && !(propsCandidate instanceof Element)) {
// args[0] is not child element or text node
// must be props object
Object.assign(element, propsCandidate);
args.shift();
}
element.append(...args);
return element;
}
export const element = new Proxy({}, {
get(_, key) {
return (...args) => createElement(key, ...args)
}
get(_, key) {
return (...args) => createElement(key, ...args)
}
});

@ -16,40 +16,24 @@
// Import magic numbers and data
import {
dom, COLUMN, COLUMN_LITERAL, COMPARISON, ATTRIBUTES,
columnAscendingByDefault, columnIsSortable, columnNeedsCasefold,
columnIsNumeric, element
columnAscendingByDefault, columnIsSortable, COLUMN_TRANSFORMATION, element
} from './js/constants.js';
// Hidden communities for transparency.
const filteredCommunities = {
tests: [
"2e9345+c7fb", // TestRoom
"762ba9+c7fb", // TesterRoom
"appletonv2+4264", // -
"b4d829+c7fb", // Test
"e5853a+c7fb", // testtest
"fishing+8e2e", // Example group from PySOGS documentation
"test+118d", // Testing 1, 2, 3
"test+13f6", // Testing room
"test+c01b", // Testing room
"test+13f6", // Testing room2
"test+fe93", // 测试Test)
"xyz+7908", // XYZ Room
"xyz+efca", // XYZ Room
],
offensive: [
"60fa60+c7fb", // "N-word" Community
"ab1a4d+c7fb", // zUnsensored Group (CSAM)
"AlexMed+e093", //
"AlexMed+e093", // drug trading?
"gore+e5e0", // gore
"RU-STEROID+e093" //
"RU-STEROID+e093" // drug trading?
],
// These communities should be checked regularly
// in case they update their PySOGS version
legacy: [
"Ukraine+02bd" // https://reccacon.com/view/room/Ukraine
]
};
// This can be achieved with `text-overflow: ellipsis` instead
@ -62,12 +46,26 @@ const transformJoinURL = (join_link) => {
});
}
function onLoad(timestamp) {
setLastChecked(timestamp);
function getTimestamp() {
const timestampRaw =
document.querySelector('meta[name=timestamp]')
?.getAttribute('content');
if (!timestampRaw) return null;
const timestamp = parseInt(timestampRaw);
if (Number.isNaN(timestamp)) return null;
return timestamp;
}
function onLoad() {
const timestamp = getTimestamp();
if (timestamp !== null) {
setLastChecked(timestamp);
}
hideBadCommunities();
sortTable(COLUMN.NAME);
createJoinLinkButtons();
markSortableColumns();
addQRModalHandlers();
}
function displayQRModal(communityID) {
@ -78,6 +76,25 @@ function hideQRModal(communityID) {
dom.qr_modal(communityID).style.display = "none";
}
function addQRModalHandlers() {
const rows = dom.tbl_communities()?.rows;
if (!rows) throw new Error("Rows not found");
for (const row of rows) {
if (row.querySelector('th')) continue;
const communityID = row.getAttribute('--data-identifier');
row.querySelector('.td_qr_code').addEventListener(
'click',
() => displayQRModal(communityID)
);
const closeButton =
dom.qr_modal(communityID).querySelector('.qr-code-modal-close');
closeButton.addEventListener(
'click',
() => hideQRModal(communityID)
);
}
}
function createJoinLinkButtons() {
const join_URLs = dom.join_urls();
Array.from(join_URLs).forEach((td_url) => {
@ -90,7 +107,7 @@ function createJoinLinkButtons() {
function hideBadCommunities() {
let numberOfHiddenCommunities = 0;
for (const category of ['tests', 'offensive', 'legacy']) {
for (const category of ['tests', 'offensive']) {
numberOfHiddenCommunities +=
filteredCommunities[category]
.map(hideElementByID)
@ -193,21 +210,12 @@ function makeRowComparer(column, ascending) {
}
// Callback to obtain sortable content from cell text.
let contentToSortable = (text) => text.trim();
if (columnNeedsCasefold(column)) {
// Make certain columns sort regardless of casing.
contentToSortable = (text) => text.toLowerCase().trim();
}
else if (columnIsNumeric(column)) {
// Make certain columns sort on parsed numeric value instead of text.
contentToSortable = (text) => parseInt(text);
}
const columnToSortable = COLUMN_TRANSFORMATION[column] ?? ((el) => el.innerText.trim());
// Construct comparer using derived property to determine sort order.
const rowComparer = compareProp(
ascending ? compareAscending : compareDescending,
row => contentToSortable(row.children[column].innerText)
row => columnToSortable(row.children[column])
);
return rowComparer;
@ -249,18 +257,27 @@ function setSortState(table, { ascending, column }) {
}
table.setAttribute(ATTRIBUTES.SORTING.ASCENDING, ascending);
table.setAttribute(ATTRIBUTES.SORTING.COLUMN, column);
// This can be used to style column headers in a consistent way, i.e.
// #tbl_communities[data-sort-asc=true][sorted-by=name]::after #th_name, ...
table.setAttribute(ATTRIBUTES.SORTING.COLUMN_LITERAL, COLUMN_LITERAL[column]);
// No way around this for brief CSS.
const headers = table.querySelectorAll("th");
headers.forEach((th, colno) => {
th.removeAttribute(ATTRIBUTES.SORTING.ACTIVE);
});
headers[column].setAttribute(ATTRIBUTES.SORTING.ACTIVE, true);
}
// This is best done in JS, as it would require <noscript> styles otherwise.
function markSortableColumns() {
const table = dom.tbl_communities();
for (const th of table.querySelectorAll('th')) {
if (th.id.includes("qr_code")) continue;
th.classList.add('sortable');
}
const header_cells = table.querySelectorAll('th');
for (let colno = 0; colno < header_cells.length; colno++) {
if (!columnIsSortable(colno)) continue;
header_cells[colno].classList.add('sortable');
header_cells[colno].addEventListener(
'click',
() => sortTable(colno)
)
};
}
/**
@ -284,13 +301,7 @@ function sortTable(column) {
setSortState(table, { ascending, column });
}
// html.js for styling purposes
window.document.documentElement.classList.add("js");
// Crude way to export from module script due to inline event handlers.
// Ideally, all handlers would be attached from JS via addEventListener.
Object.assign(window, {
onLoad, sortTable, displayQRModal,
hideQRModal, copyToClipboard
});
// `html.js` selector for styling purposes
document.documentElement.classList.add("js");
document.addEventListener('DOMContentLoaded', () => onLoad());

@ -2,10 +2,19 @@ html {
font-size: clamp(10px, 2vw, 18px);
}
html.js .noscript {
html.js .noscript, .hidden {
display: none;
}
.clickable {
cursor: pointer;
}
.anchorstyle {
color: rgb(0, 102, 204);
text-decoration: underline;
}
/* Dead style */
html:not(.js) .js-only {
display: none;
@ -23,12 +32,62 @@ header {
flex-grow: 1;
}
#tbl_communities { width:100%; }
#tbl_communities {
/* Browser defaults. */
--cell-padding-h: 1px;
--cell-padding-v: 1px;
width:100%;
}
#tbl_communities th {
white-space: nowrap;
}
#tbl_communities :is(th, td) {
padding: var(--cell-padding-v) var(--cell-padding-h);
}
#tbl_communities th.sortable {
position: relative;
padding-right: calc( 1.5em + var(--cell-padding-h) );
}
#tbl_communities th.sortable::after {
position: absolute;
right: 0.25em;
top: 50%;
transform: translateY(-50%);
/* content: "\25C7"; */ /* White diamond */
/* content: "\2195"; */ /* Up-down arrow */
/* content: "\25A1"; */ /* White square */
/* content: "\25B8"; */ /* Small right pointing triangle */
content: "\2B25"; /* Black medium diamond */
color: grey;
}
/* Hide the identifier column before removal. */
#th_identifier { display: none; }
.td_identifier { display: none; font-family: monospace; }
#tbl_communities[data-sort-asc=true] th[data-sort=true]::after {
content: "\25B2"; /* Black up pointing triangle */
color: initial;
}
#tbl_communities[data-sort-asc=false] th[data-sort=true]::after {
content: "\25BC"; /* Black up pointing triangle */
color: initial;
}
#toggle-show-room-ids:not(:checked)
~ #tbl_communities :is(#th_identifier, .td_identifier) {
display: none;
}
.td_identifier {
font-family: monospace;
}
.td_language {
text-align: center;
font-size: 1.25em;
}
.td_language:empty::after {
content: "\2753";
}
@ -43,8 +102,29 @@ header {
}
.td_users { text-align: right; }
.td_preview { text-align: center; }
.td_users { text-align: right; }
.td_preview { text-align: center; }
.td_server_icon { text-align: center; }
.td_server_icon-circle {
display: flex;
align-items: center;
justify-content: center;
width: 2em;
height: 2em;
border-radius: 2em;
font-family: sans-serif;
margin: 0 auto;
color: white;
text-shadow: 0 0 0.5em #000;
box-shadow: 0 0 0.05em #777;
}
.td_server_icon-circle span {
position: relative;
top: 0.05em;
}
.td_join_url {
font-family: monospace;
white-space: nowrap;
@ -70,7 +150,7 @@ header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
justify-content: space-around;
}
.copy_button { font-size: inherit }
@ -95,12 +175,17 @@ footer nav a {
white-space: nowrap;
}
label[for=toggle-show-room-ids]::after {
content: " (Off)"
}
#toggle-show-room-ids:checked
~ footer label[for=toggle-show-room-ids]::after {
content: " (On)"
}
/* <Colors> */
:root {
/*--session-classic-dark-green: #31f196;*/
/*--session-classic-dark-gray-one: #414141;*/
/*--session-classic-dark-gray-two: #2d2d2d;*/
/*--session-classic-dark-gray-three: #1b1b1b;*/
--alternate-row-color: #e8e8e8;
}
#tbl_communities th { background-color: lightgray; }
@ -209,3 +294,4 @@ footer nav a {
to {bottom: 0; opacity: 0;}
}

@ -1,12 +1,23 @@
<?php
$PROJECT_ROOT = dirname(__FILE__);
while (!file_exists("$PROJECT_ROOT/.phpenv")) {
if ($PROJECT_ROOT == "/" || $PROJECT_ROOT == "")
throw new RuntimeException("Could not find .phpenv file.");
$PROJECT_ROOT = dirname($PROJECT_ROOT);
}
(function(){
global $PROJECT_ROOT;
$root_previous = "";
while (!file_exists("$PROJECT_ROOT/.phpenv")) {
if (
$PROJECT_ROOT == "/" ||
$PROJECT_ROOT == "" ||
$PROJECT_ROOT == $root_previous
)
throw new RuntimeException("Could not find .phpenv file.");
$root_previous = $PROJECT_ROOT;
$PROJECT_ROOT = dirname($PROJECT_ROOT);
}
})();
require_once "$PROJECT_ROOT/.phpenv";
// set_include_path(get_include_path() . PATH_SEPARATOR . $PROJECT_ROOT);

@ -41,6 +41,9 @@
// Removed by request:
// "http://116.203.51.179",
// "http://bitcoincash.tokyo/",
// Removed out of decency:
// [redacted]
);
$KNOWN_PUBKEYS = array(

@ -3,6 +3,7 @@
include_once "$PROJECT_ROOT/languages/language_flags.php";
class CommunityRoom implements JsonSerializable {
/** @var CommunityServer $server */
public readonly object $server;
public readonly ?int $active_users;
public readonly ?int $active_users_cutoff;
@ -400,6 +401,10 @@
return url_get_base($this->base_url, include_scheme: false);
}
function get_base_url() {
return $this->base_url;
}
function get_pubkey() {
return $this->pubkey;
}

@ -1,3 +1,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="favicon.svg" sizes="any">
<link rel="icon" type="image/svg+xml" href="favicon.svg" sizes="any">
<meta name="keywords" content="Session, Community, Anonymous, Chat">
<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';
"
>

@ -31,7 +31,7 @@
<?php foreach ($rooms as $room): ?>
<div id="modal_<?=$room->get_room_identifier()?>" class="qr-code-modal">
<div class="qr-code-modal-content">
<span class="qr-code-modal-close" onclick='hideQRModal("<?=$room->get_room_identifier()?>")'>
<span class="qr-code-modal-close">
&times;
</span>
<img

@ -8,16 +8,18 @@
// Once handlers are attached in JS, this check ceases to be useful.
function column_sortable($id) {
return $id != "qr";
// Join URL contents are not guaranteed to have visible text.
return $id != "qr" && $id != "preview" && $id != "join_url";
}
function sort_onclick($colno) {
global $TABLE_COLUMNS;
$column = $TABLE_COLUMNS[$colno];
if (!column_sortable($column['id'])) return "";
return " onclick='sortTable($colno)'";
$name = $column['name'];
return " title='Click to sort by $name'";
}
$TABLE_COLUMNS = [
['id' => "identifier", 'name' => "Identifier"],
['id' => "language", 'name' => "L"],
@ -26,6 +28,7 @@
['id' => "users", 'name' => "Users"],
['id' => "preview", 'name' => "Preview"],
['id' => "qr", 'name' => "QR"],
['id' => "server_icon", 'name' => "Server"],
['id' => "join_url", 'name' => "Join URL"],
];
?>
@ -39,37 +42,66 @@
</th>
<?php endforeach; ?>
</tr>
<?php foreach ($rooms as $room): ?>
<tr id="<?=$room->get_room_identifier()?>">
<td class="td_identifier"><?=$room->get_room_identifier()?></td>
<td class="td_language"><?=$room->language_flag?></td>
<td class="td_name"><?=$room->name?></td>
<td class="td_description"
><?=$room->description?></td>
<td class="td_users"><?=$room->active_users?></td>
<td class="td_preview">
<a href="<?=$room->get_preview_url()?>" target="_blank" rel="noopener noreferrer">
<?php if (str_starts_with($room->get_preview_url(), 'http://')): ?>
<span class="protocol-indicator protocol-http">HTTP</span>
<?php endif; ?>
<?php if (str_starts_with($room->get_preview_url(), 'https://')): ?>
<span class="protocol-indicator protocol-https">HTTPS</span>
<?php endif; ?>
<?php foreach ($rooms as $id => $room): ?>
<?php
// TODO: Do not forget to rename this escape when merging!
$token = $room->server->get_pubkey();
$icon_hue = hexdec($token[2] . $token[2]);
$icon_color = "hsl($icon_hue, 80%, 50%)";
$hostname = $room->server->get_base_url();
// Escape external input.
// Ternaries prevent passing null-equal strings, which produce warnings.
$id = htmlspecialchars($room->get_room_identifier());
$language = $room->language_flag ? htmlspecialchars($room->language_flag) : "";
$name = htmlspecialchars($room->name);
$desc = $room->description ? htmlspecialchars($room->description) : "";
$users = htmlspecialchars($room->active_users);
$preview_link = htmlspecialchars($room->get_preview_url());
$join_link = htmlspecialchars($room->get_invite_url());
// TODO: Do not forget to rename this escape when merging!
$token = htmlspecialchars($token);
$hostname = htmlspecialchars($hostname);
?>
<tr id="<?=$id?>" itemscope itemtype="https://schema.org/EntryPoint" --data-identifier="<?=$id?>">
<td class="td_identifier" itemprop="identifier"><?=$id?></td>
<td class="td_language"><?=$language?></td>
<td class="td_name" itemprop="name"><?=$name?></td>
<td class="td_description" itemprop="description"><?=$desc?></td>
<td class="td_users"><?=$users?></td>
<td class="td_preview" itemprop="url">
<a href="<?=$preview_link?>" target="_blank" rel="noopener noreferrer nofollow">
<?php if (str_starts_with($room->get_preview_url(), 'http://')): ?>
<span class="protocol-indicator protocol-http">HTTP</span>
<?php endif; ?>
<?php if (str_starts_with($room->get_preview_url(), 'https://')): ?>
<span class="protocol-indicator protocol-https">HTTPS</span>
<?php endif; ?>
</a>
</td>
<td class="td_qr_code">
<img
class="qr-code-icon"
<img
class="qr-code-icon"
src="qrcode-solid.svg"
onclick='displayQRModal("<?=$room->get_room_identifier()?>")'
alt="Pictogram of a QR code"
>
</td>
<td class="td_server_icon"
data-token="<?=$token?>"
title="<?=$hostname?> (<?=$token?>)"
item="image"
>
<div class="td_server_icon-circle" style="background-color: <?=$icon_color?>">
<span><?=strtoupper($token[0] . $token[1])?></span>
</div>
</td>
<td class="td_join_url">
<div class="join_url_container" data-url="<?=$room->get_join_url()?>">
<a class="join_url show-from-w5" title="<?=$room->get_join_url()?>"
><?=truncate($room->get_join_url(), 32)?></a>
<a class="noscript" href="<?=$room->get_join_url()?>"
<div class="join_url_container" data-url="<?=$join_link?>">
<a class="join_url show-from-w5" title="<?=$join_link?>"
><?=truncate($join_link, 32)?></a>
<a class="noscript" href="<?=$join_link?>" rel="external nofollow"
>Copy link</a>
</div>
</td>

@ -0,0 +1,12 @@
Mobiililaitteella:
- Napsauta Kopioi-nappia kopioidaksesi ryhmän URL-osoitteen.
- Avaa Session, paina plussan painiketta ja valitse "Liity ryhmään" ("Join Community").
- Liitä kopioitu URL-osoite "Syötä yhteisön URL-osoite"-kenttään, ja paina"Liity".
Mobiililaitteella, kun tämä sivusto on auki tietokoneellasi:
- Paina valitsemasi yhteisön QR-koodipainiketta tietokoneellasi.
- Avaa Session, paina plussan painiketta ja valitse "Liity ryhmään" ("Join Community").
- Valitse ylhäältä "Skannaa QR-koodi" ja skannaa puhelimen kameralla ryhmän QR-koodiin.

@ -0,0 +1,12 @@
На мобильном девайсе:
- Нажмите кнопку «Копировать», чтобы скопировать URL-адрес нужной группы.
- Откройте Session, нажмите на кнопку с плюсом и выберите «Присоединиться к сообществу».
- Вставьте скопированный URL-адрес в поле «Введите URL-адрес сообщества», и нажмите «Присоединиться».
На мобильном девайсе, когда этот сайт открыт на вашем компьютере:
- Нажмите кнопку QR-кода для выбранного сообщества на вашем компьютере.
- Откройте Session, нажмите на кнопку с плюсом и выберите «Присоединиться к сообществу».
- Выберите «Сканировать QR-код» наверху и наведите камеру телефона на QR-код.

@ -16,17 +16,35 @@
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="https://sessioncommunities.online/">
<link rel="stylesheet" href="styles2.css">
<script type="module" src="main.js"></script>
<script type="module" src="./main.js"></script>
<title>Self-updating list of active Session communities</title>
<meta name="description" content="
Directory of Session Open Groups — public chatrooms within Session Messenger.
Copy these Communities into the Session app
and talk anonymously about Privacy, Security, or Cryptocurrency.
">
<meta name="modified" content="<?=date("Y-m-d H:i:s", $timestamp)?>">
<meta property="og:title" content="Click here for Session Communities">
<meta
property="og:description"
content="<?=count($rooms_assoc)?> Communities and counting — updated every day!"
>
<meta property="og:url" content="https://sessioncommunities.online/">
<meta property="og:type" content="website">
<meta property="og:locale" content="en_US"/>
<meta name="timestamp" content="<?=$timestamp?>">
</head>
<body onload="onLoad(<?php echo $timestamp ?>)">
<body>
<header>
<div id="header-start"></div>
<div id="header-end">
<a
id="link-instructions"
target="_blank"
rel="help"
title="Mutli-language guide to joining communities using the site."
href="instructions.html"
>Instructions</a>
</div>
@ -34,6 +52,8 @@
<h1 id="headline">Session Communities</h1>
<?php include "+components/qr_modals.php" ?>
<input type="checkbox" id="toggle-show-room-ids" class="hidden">
<?php include "+components/tbl_communities.php" ?>
<hr>
@ -51,10 +71,16 @@
</p>
<p id="disclaimer">
This site is not affiliated with
<a href="https://optf.ngo">Oxen Privacy Tech Foundation</a>.
<a
href="https://optf.ngo"
target="_blank"
>Oxen Privacy Tech Foundation</a>.
<br>
Communities shown are fetched automatically from
various sources.
<a
href="https://github.com/mdPlusPlus/sessioncommunities.online#which-sources-are-crawled"
target="_blank"
>various sources</a>.
<br>
We make an attempt to hide communities containing
objectionable or illegal content, but
@ -65,6 +91,16 @@
However, some interactive features are
only available with JS enabled.
</p>
<p>
<label
for="toggle-show-room-ids"
class="clickable anchorstyle"
tabindex="0"
title="Shows a column with community identifiers
for developers or language data contributors.">
Toggle room identifier display
</label>
</p>
<nav>
<a
href="https://lokilocker.com/Mods/Session-Groups/wiki/Session-Closed-Groups"
@ -79,20 +115,32 @@
<a
href="https://github.com/oxen-io/session-pysogs"
target="_blank"
title="Information about running a community server"
title="Information about running a community server."
>Host Your Own Community</a>
<a
href="https://getsession.org/terms-of-service"
target="_blank"
>Session Terms Of Service</a>
</nav>
<nav>
<a
href="https://github.com/mdPlusPlus/sessioncommunities.online"
target="_blank"
title="sessioncommunities.online repository on GitHub."
>Source Code & Contact</a>
>Source Code</a>
<a
href="https://lokilocker.com/someguy/sessioncommunities.online"
target="_blank"
title="sessioncommunities.online repository on Lokinet Gitea."
>Source Code (Mirror)</a>
<a
href="https://github.com/mdPlusPlus/sessioncommunities.online#contact"
target="_blank"
rel="author"
title="Information on how to contact the maintainer of sessioncommunities.online"
>Contact</a>
</nav>
</footer>
<div id="copy-snackbar">
Copied URL to clipboard. Paste into Session app to join
</div>

@ -1,46 +1,66 @@
<?php
include_once "+getenv.php";
$instruction_files = glob("+instructions/*.txt");
function file_language($file) { return pathinfo($file)['filename']; }
?>
<!DOCTYPE html>
<html>
<head>
<?php include "+components/page-head.php" ?>
<link rel="canonical" href="https://sessioncommunities.online/instructions.php">
<link rel="stylesheet" href="css/instructions.css">
<style type="text/css">
<?php foreach ($instruction_files as $i => $file): ?>
#language-selection-<?=$i?>:checked ~
#language-selection-<?=$i?>:checked ~
#instructions #instructions-<?=$i?> {
display: block;
}
<?php endforeach; ?>
</style>
<meta name="description" content="
Discover how to use the sessioncommunities.online website
to help you join Session Open Groups; available in <?php
// Print languages instructions are available in.
$languages = array_map('file_language', array_slice($instruction_files, 0, 10));
echo(join(", ", $languages));
?>. In short: Use the Copy button or scan the QR code!
">
<meta property="og:title" content="How to join Session Communities">
<meta property="og:description" content="Learn how to use sessioncommunities.online to join">
<meta property="og:url" content="https://sessioncommunities.online/instructions.php">
<meta property="og:type" content="article">
</head>
<body>
<header>
<h1>Instructions for joining Session Communities</h1>
</header>
<main>
<p>
<a href="/" title="Return to the Session Community listing">
Go back to Community list
</a>
</p>
Choose your language:
<?php foreach ($instruction_files as $i => $file): ?>
<br>
<input
<input
id="language-selection-<?=$i?>"
class="language-selection"
name="language"
type="radio"
<?=file_language($file) == 'English' ? 'checked="checked"' : ''?>
>
<label for="language-selection-<?=$i?>">
<?=
// Name of the language
// Can be later parsed from i.e. first line of file
pathinfo($file)['filename']
file_language($file);
?>
</label>
<?php endforeach; ?>
<article id="instructions">
<?php foreach ($instruction_files as $i => $file): ?>
<section id="instructions-<?=$i?>" class="instructions"><?php