diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1cb5dc1 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 ` +- `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 diff --git a/README.md b/README.md index eebc2cf..0cab54f 100644 --- a/README.md +++ b/README.md @@ -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/ +- +- +- -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: +- Lokinet Gitea: + +If your favourite Session community is missing a language flag, you can issue a pull request here: -- 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`. diff --git a/languages b/languages index 524ad98..f2aaed4 160000 --- a/languages +++ b/languages @@ -1 +1 @@ -Subproject commit 524ad983610b096ed09e721c20cba011dfbc7992 +Subproject commit f2aaed46ea2ef3a6ff03b6eee6f70c81cc20f069 diff --git a/output/js/constants.js b/output/js/constants.js index 8fe3d67..49c040a 100644 --- a/output/js/constants.js +++ b/output/js/constants.js @@ -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 any>} + */ +const TRANSFORMATION = { + numeric: (el) => parseInt(el.innerText), + casefold: (el) => el.innerText.toLowerCase().trim(), + tokenData: (el) => el.getAttribute("data-token") +} + +/** + * @type {Dictionary 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) + } }); diff --git a/output/main.js b/output/main.js index f6a341a..3f27bf3 100644 --- a/output/main.js +++ b/output/main.js @@ -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