Initial commit — Singular Particular Space v1

Homepage (site/index.html): integration-v14 promoted, Writings section
integrated with 33 pieces clustered by type (stories/essays/miscellany),
Writings welcome lightbox, content frame at 98% opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 12:09:22 +02:00
commit 5422131782
359 changed files with 117437 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
{
"manifest_version": 3,
"name": "Edge Tab Catcher",
"version": "1.0.0",
"description": "Export all open tabs with their URLs, titles, and descriptions to a Markdown file",
"permissions": [
"tabs",
"activeTab",
"scripting",
"downloads"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"icons": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
},
"author": "Your Name",
"homepage_url": "https://github.com/yourusername/tab-exporter"
}

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tab Exporter</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="container" role="main">
<header>
<h1 id="main-heading">Tab Exporter</h1>
<p class="subtitle">Export all your open tabs to a Markdown file</p>
</header>
<section class="stats" aria-labelledby="stats-heading">
<h2 id="stats-heading" class="visually-hidden">Tab Statistics</h2>
<div class="stat-card" role="status" aria-live="polite">
<span class="stat-number" id="tab-count">0</span>
<span class="stat-label">Open Tabs</span>
</div>
<div class="stat-card" role="status" aria-live="polite">
<span class="stat-number" id="window-count">0</span>
<span class="stat-label">Windows</span>
</div>
</section>
<section class="options" aria-labelledby="options-heading">
<h2 id="options-heading" class="section-heading">Export Options</h2>
<div class="option-group">
<label class="checkbox-label">
<input
type="checkbox"
id="include-descriptions"
checked
aria-describedby="desc-help"
>
<span>Include page descriptions</span>
</label>
<p id="desc-help" class="help-text">Fetches meta descriptions from each tab (may take longer)</p>
</div>
<div class="option-group">
<label class="checkbox-label">
<input
type="checkbox"
id="group-by-window"
aria-describedby="window-help"
>
<span>Group tabs by window</span>
</label>
<p id="window-help" class="help-text">Organizes tabs under separate window headings</p>
</div>
<div class="option-group">
<label class="checkbox-label">
<input
type="checkbox"
id="include-timestamp"
checked
aria-describedby="time-help"
>
<span>Include export timestamp</span>
</label>
<p id="time-help" class="help-text">Adds date and time to the exported file</p>
</div>
</section>
<section class="actions">
<button
id="export-btn"
class="btn btn-primary"
aria-describedby="export-status"
>
<span class="btn-icon" aria-hidden="true">📥</span>
Export Tabs
</button>
<div
id="export-status"
class="status-message"
role="status"
aria-live="polite"
aria-atomic="true"
></div>
</section>
<footer class="footer">
<p class="footer-text">
<kbd>Ctrl+Click</kbd> a tab to exclude it from export
</p>
</footer>
</main>
<script src="popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,207 @@
// Initialize extension
document.addEventListener('DOMContentLoaded', async () => {
await updateStats();
setupEventListeners();
});
// Update tab and window count statistics
async function updateStats() {
try {
const tabs = await chrome.tabs.query({});
const windows = await chrome.windows.getAll();
document.getElementById('tab-count').textContent = tabs.length;
document.getElementById('window-count').textContent = windows.length;
} catch (error) {
console.error('Error updating stats:', error);
}
}
// Setup event listeners
function setupEventListeners() {
const exportBtn = document.getElementById('export-btn');
exportBtn.addEventListener('click', handleExport);
// Keyboard accessibility for options
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
checkbox.checked = !checkbox.checked;
}
});
});
}
// Main export handler
async function handleExport() {
const exportBtn = document.getElementById('export-btn');
const statusEl = document.getElementById('export-status');
// Get user options
const includeDescriptions = document.getElementById('include-descriptions').checked;
const groupByWindow = document.getElementById('group-by-window').checked;
const includeTimestamp = document.getElementById('include-timestamp').checked;
// Disable button and show loading state
exportBtn.disabled = true;
const originalContent = exportBtn.innerHTML;
exportBtn.innerHTML = '<span class="btn-icon loading" aria-hidden="true">⏳</span>Exporting...';
statusEl.textContent = 'Collecting tab information...';
statusEl.className = 'status-message loading';
try {
// Get all tabs
const tabs = await chrome.tabs.query({});
// Fetch descriptions if needed
if (includeDescriptions) {
statusEl.textContent = `Fetching descriptions (0/${tabs.length})...`;
await fetchDescriptions(tabs, statusEl);
}
// Generate markdown content
const markdown = await generateMarkdown(tabs, {
includeDescriptions,
groupByWindow,
includeTimestamp
});
// Trigger download
downloadMarkdown(markdown);
// Show success message
statusEl.textContent = '✓ Successfully exported ' + tabs.length + ' tabs!';
statusEl.className = 'status-message success';
} catch (error) {
console.error('Export error:', error);
statusEl.textContent = '✗ Error: ' + error.message;
statusEl.className = 'status-message error';
} finally {
// Re-enable button
exportBtn.disabled = false;
exportBtn.innerHTML = originalContent;
}
}
// Fetch descriptions from tabs
async function fetchDescriptions(tabs, statusEl) {
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
statusEl.textContent = `Fetching descriptions (${i + 1}/${tabs.length})...`;
try {
// Try to execute script to get description
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: extractDescription
});
if (results && results[0] && results[0].result) {
tab.description = results[0].result;
}
} catch (error) {
// Tab might not support script injection (e.g., chrome:// pages)
tab.description = null;
}
}
}
// Function to extract description (runs in tab context)
function extractDescription() {
const metaDesc = document.querySelector('meta[name="description"]');
const ogDesc = document.querySelector('meta[property="og:description"]');
return (metaDesc && metaDesc.content) ||
(ogDesc && ogDesc.content) ||
null;
}
// Generate markdown content
async function generateMarkdown(tabs, options) {
let markdown = '# Browser Tabs Export\n\n';
if (options.includeTimestamp) {
const now = new Date();
const timestamp = now.toLocaleString('en-US', {
dateStyle: 'full',
timeStyle: 'short'
});
markdown += `**Exported:** ${timestamp}\n\n`;
}
markdown += `**Total Tabs:** ${tabs.length}\n\n`;
markdown += '---\n\n';
if (options.groupByWindow) {
// Group tabs by window
const windows = await chrome.windows.getAll();
const tabsByWindow = {};
tabs.forEach(tab => {
if (!tabsByWindow[tab.windowId]) {
tabsByWindow[tab.windowId] = [];
}
tabsByWindow[tab.windowId].push(tab);
});
let windowIndex = 1;
for (const windowId in tabsByWindow) {
markdown += `## Window ${windowIndex}\n\n`;
markdown += formatTabs(tabsByWindow[windowId], options.includeDescriptions);
markdown += '---\n\n';
windowIndex++;
}
} else {
// List all tabs together
markdown += formatTabs(tabs, options.includeDescriptions);
}
return markdown;
}
// Format tabs as markdown
function formatTabs(tabs, includeDescriptions) {
let content = '';
tabs.forEach((tab, index) => {
// List item with title and URL
content += `- **${escapeMarkdown(tab.title)}**\n`;
content += ` - URL: ${tab.url}\n`;
// Description if available
if (includeDescriptions && tab.description) {
content += ` - Description: ${escapeMarkdown(tab.description)}\n`;
}
content += '\n';
});
return content;
}
// Escape markdown special characters
function escapeMarkdown(text) {
if (!text) return '';
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
}
// Download markdown file
function downloadMarkdown(content) {
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const filename = `tabs-export-${timestamp}.md`;
chrome.downloads.download({
url: url,
filename: filename,
saveAs: true
}, (downloadId) => {
// Cleanup object URL after download starts
setTimeout(() => URL.revokeObjectURL(url), 100);
});
}

View File

@@ -0,0 +1,279 @@
:root {
--primary-color: #2d8659;
--primary-hover: #36a169;
--primary-active: #236b47;
--secondary-color: #0d9488;
--alternate-color: #c4b5fd;
--success-color: #2d8659;
--error-color: #ef4444;
--text-primary: #bfdbfe;
--text-secondary: #93c5fd;
--bg-primary: linear-gradient(135deg, #1e1b4b 0%, #1e3a8a 100%);
--bg-secondary: rgba(255, 255, 255, 0.05);
--border-color: rgba(191, 219, 254, 0.2);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.4);
--radius: 8px;
--transition: all 0.2s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg-primary);
width: 380px;
min-height: 400px;
}
/* Visually hidden but accessible to screen readers */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.container {
padding: 20px;
}
header {
text-align: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid rgba(196, 181, 253, 0.3);
}
h1 {
font-size: 24px;
font-weight: 600;
color: var(--alternate-color);
margin-bottom: 4px;
}
.subtitle {
font-size: 13px;
color: var(--text-secondary);
}
/* Stats Section */
.stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-secondary);
border-radius: var(--radius);
padding: 16px;
text-align: center;
transition: var(--transition);
border: 1px solid var(--border-color);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.stat-number {
display: block;
font-size: 28px;
font-weight: 700;
color: var(--secondary-color);
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
display: block;
font-size: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Options Section */
.options {
margin-bottom: 24px;
}
.section-heading {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-primary);
}
.option-group {
margin-bottom: 16px;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
padding: 8px;
border-radius: 6px;
transition: var(--transition);
}
.checkbox-label:hover {
background: rgba(255, 255, 255, 0.08);
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
margin-right: 10px;
cursor: pointer;
accent-color: var(--primary-color);
border: 1px solid var(--border-color);
}
.checkbox-label input[type="checkbox"]:focus {
outline: 2px solid var(--alternate-color);
outline-offset: 2px;
}
.checkbox-label span {
font-size: 14px;
font-weight: 500;
}
.help-text {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
margin-left: 28px;
}
/* Actions Section */
.actions {
margin-bottom: 16px;
}
.btn {
width: 100%;
padding: 14px 20px;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: var(--radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:focus {
outline: 2px solid var(--alternate-color);
outline-offset: 2px;
}
.btn-primary {
background: var(--primary-color);
color: white;
box-shadow: var(--shadow);
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-hover);
box-shadow: var(--shadow-hover);
transform: translateY(-1px);
}
.btn-primary:active:not(:disabled) {
background: var(--primary-active);
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-icon {
font-size: 18px;
}
.status-message {
margin-top: 12px;
padding: 12px;
border-radius: 6px;
font-size: 13px;
text-align: center;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.status-message.success {
background: rgba(45, 134, 89, 0.2);
color: #6ee7b7;
border: 1px solid var(--success-color);
}
.status-message.error {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid var(--error-color);
}
.status-message.loading {
background: rgba(13, 148, 136, 0.2);
color: var(--secondary-color);
border: 1px solid var(--secondary-color);
}
/* Footer */
.footer {
padding-top: 16px;
border-top: 1px solid rgba(196, 181, 253, 0.3);
}
.footer-text {
font-size: 12px;
color: var(--text-secondary);
text-align: center;
}
kbd {
display: inline-block;
padding: 2px 6px;
font-family: monospace;
font-size: 11px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
color: var(--alternate-color);
}
/* Loading spinner animation */
@keyframes spin {
to { transform: rotate(360deg); }
}
.btn-icon.loading {
animation: spin 1s linear infinite;
}

37
ToolsnToys/Meshagora/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.Python
# Virtual environments
venv/
.venv/
env/
.env/
# Distribution / packaging
*.egg-info/
dist/
build/
# Environment variables
.env
.env.*
# OS
.DS_Store
Thumbs.db
# Editor
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
# Upgrade / internal notes
upgrade_notes.md

View File

@@ -0,0 +1,288 @@
# Meshagora — Deployment & Troubleshooting
---
## Version History
### v0.2 — IP Detection & Startup Transparency
- Replaced UDP routing trick with `ifconfig` parsing for hotspot IP detection. The UDP trick failed on the Samsung A25 (and likely other Android builds) because the hotspot interface routing entry may not exist at startup. `ifconfig` reads interface addresses directly via socket ioctls — no netlink, no root, no routing assumptions.
- Startup banner now lists all detected non-loopback IPs with interface names, so if the primary guess is wrong the alternatives are immediately visible.
- Added `install_termux.sh` — one-shot setup script for Termux.
- Added `net-tools` as an explicit dependency (`ifconfig` provider).
### v0.1 — Initial prototype
- Single-file Flask app, in-memory SQLite, runs on Android/Termux hotspot.
---
## Dependencies
### Termux (Android) — required packages
| Package | Source | Purpose |
|---------|--------|---------|
| `python` | `pkg` | Runtime |
| `net-tools` | `pkg` | `ifconfig` — hotspot IP detection |
| `libjpeg-turbo` | `pkg` | JPEG support for Pillow (required by `qrcode[pil]`) |
| `libpng` | `pkg` | PNG support for Pillow (required by `qrcode[pil]`) |
| `flask` | `pip` | Web framework |
| `qrcode[pil]` | `pip` | QR code generation at `/qr` |
### Desktop (testing only)
| Package | Source | Purpose |
|---------|--------|---------|
| `flask` | `pip` | Web framework |
| `qrcode[pil]` | `pip` | QR code generation |
`ifconfig` is standard on desktop Linux/macOS — no extra install needed.
---
## Quick Start (Termux)
**First time only — install dependencies:**
```
bash install_termux.sh
```
Or manually:
```
pkg install python net-tools libjpeg-turbo libpng
pip install flask "qrcode[pil]"
```
**Every session:**
1. Enable Android mobile hotspot.
2. Open Termux and run:
```
python3 meshagora.py
```
3. Read the startup banner — note all three keys and the Join URL.
4. Open `http://[IP]:5000/qr` on the server device and display it for participants to scan.
5. Once everyone has joined, go to `/admin` → HIDE QR.
---
## Starting the Server
1. **Enable the Android mobile hotspot before running the script.**
IP detection reads active interfaces at startup — if the hotspot is off, the banner URL will be wrong.
2. Run:
```
python3 meshagora.py
```
3. The startup banner prints:
```
==================================================
MESHAGORA v2.3 — SOCIAL SANDBOX
Admin Post Key : [4 emoji]
Trusted Post Key : [2 emoji]
Default Post Key : [2 emoji]
--------------------------------------------------
Join URL : http://192.168.43.1:5000/join (wlan0)
Also try : http://10.0.2.5:5000/join (rmnet0)
QR code : http://192.168.43.1:5000/qr
If no URL works, run: ifconfig | grep inet
==================================================
```
Write down or photograph all three keys. They cannot be recovered without restarting the server (which destroys the session).
**Reading the banner:**
- "Join URL" is the best-guess hotspot address (`192.168.x.x` preferred).
- "Also try" lines show other detected interfaces. The `rmnet` / `10.x.x.x` entries are mobile data — participants on the hotspot WiFi cannot reach those.
- Samsung A25 hotspot is typically `192.168.43.1` on `wlan0`.
4. Navigate to `http://[IP]:5000/qr` on the server device and display the QR code for participants to scan.
5. Once all participants have joined, hide the QR code from the Admin Panel (`/admin` → HIDE QR).
---
## Connecting Participants
Participants need to:
1. Join the hotspot WiFi (name set by the server device's hotspot settings).
2. Open a browser and scan the QR code, or type the Join URL from the banner.
3. Go to `/join` — choose a username, Profile Key (2 emoji), and Friend Key (2 emoji).
No app install required. Works on any browser from Android 7.0+, iOS Safari, desktop Chrome/Firefox.
---
## Admin Panel
Navigate to `/admin` from any browser connected to the hotspot.
Enter the 4-emoji Admin Post Key when prompted. Authentication persists for that browser session.
**Actions available:**
- Restrict / Unrestrict / Promote / Demote individual users
- View the current Default Post Key (large, readable — copy to board or announce verbally)
- Rotate Default Key manually
- Trigger Flood Attack (creates 5 bot accounts with posts)
- Show / Hide the join QR code
- View raw database tables (users, posts, votes, reports, keys)
- **WHAT THE PLATFORM SEES** — surveillance dashboard (scroll to bottom of admin panel):
- *Deanonymization table*: maps every session label (Anon #N) to real username, tier, post count, and net score
- *Social graph*: all handshake pairs with CONFIRMED / PENDING status
- *Behavioral correlations*: who voted for whom and who reported whom, cross-referenced with confirmed friendships — project mid-session for maximum pedagogical impact
---
## Stopping the Server
`Ctrl+C` in the Termux terminal. The in-memory database is destroyed immediately.
There is no persistent storage — every session starts clean.
---
## Troubleshooting
### Server prints `127.0.0.1` instead of the hotspot IP
**Important:** `127.0.0.1` is the loopback address — it only works in the browser on the server device itself. Participants on other devices will get "connection refused". Flask is still listening on all interfaces (`0.0.0.0`), so the server is reachable — you just need the correct IP.
**Step 1:** Enable the mobile hotspot *before* running `python3 meshagora.py`, then restart. The hotspot interface only appears after the hotspot is on.
**Step 2:** If still wrong, check all interfaces manually:
```
ifconfig | grep inet
```
Look for an address like `192.168.43.1` under `wlan0` — that's the hotspot gateway. Ignore `127.0.0.1` (loopback) and `10.x.x.x` under `rmnet` (mobile data).
Note: `ip addr` fails in Termux with "cannot bind netlink socket: permission denied" — always use `ifconfig` instead.
**Step 3:** Navigate to `http://[correct-ip]:5000/join` directly and distribute that URL to participants.
**Samsung A-series:** The A25 hotspot is typically `192.168.43.1` on `wlan0`. If the banner's "Join URL" shows a different address, check the "Also try" lines for the `192.168.43.1` entry.
---
### `ModuleNotFoundError: No module named 'qrcode'`
```
pip install "qrcode[pil]"
```
If it installs but the error persists:
```
python3 -m pip install "qrcode[pil]"
```
The server runs without qrcode — `/qr` returns a 404. Participants can type the join URL directly.
---
### `ModuleNotFoundError: No module named 'flask'`
```
pip install flask
```
---
### `ifconfig` command not found
```
pkg install net-tools
```
This is required for hotspot IP detection in Termux. Without it, the banner will fall back to `127.0.0.1`.
---
### Port 5000 already in use
Common on macOS (AirPlay Receiver uses 5000).
**Option A:** Kill the other process:
```
lsof -i :5000
kill [PID]
```
**Option B:** Change the port at the bottom of `meshagora.py`:
```python
app.run(host='0.0.0.0', port=5001, debug=False, threaded=False)
```
Update the join URL accordingly.
---
### Participants can't reach the server
1. Confirm participants are connected to the *hotspot WiFi*, not their mobile data or a different network.
2. Confirm the IP in the banner matches `ifconfig | grep inet` output for the `wlan0` interface.
3. Confirm port 5000 is not blocked by a firewall. On Termux this is rarely an issue on a local hotspot.
4. Try navigating to the URL from the server device's own browser first to confirm the server is responding.
---
### Android kills the server mid-session (Termux background kill)
Android aggressively kills background processes to save battery.
**Prevention:**
- In Android Settings → Apps → Termux → Battery → set to "Unrestricted" (wording varies by device).
- Keep Termux in the foreground with the screen on, or acquire a wakelock:
```
termux-wake-lock
```
(Requires the Termux:API app from F-Droid.)
- Keep the server device plugged in to power.
If the server is killed, all session data is lost. Restart with `python3 meshagora.py` — all keys rotate, all users must rejoin.
---
### Default Post Key stopped working for everyone
A user was restricted (by admin action or by 51% community reports). The Default Post Key rotates silently on every restriction. Check the Admin Panel for the new key and distribute it.
---
### Emoji picker doesn't appear / looks broken
The emoji picker is rendered as inline HTML buttons with basic CSS. No JavaScript framework required.
- If buttons appear but emoji don't render: the device font doesn't support those codepoints. This is rare on Android 7.0+ but possible on stripped-down browsers. Participants can type emoji directly into the visible input field.
- If the grid looks misaligned: the scroll container (`max-height: 160px; overflow-y: auto`) is intentional. Scroll within the picker to see all emoji.
---
### Post fails with "Post key incorrect." for a RESTRICTED user
RESTRICTED users require an admin co-sign. The admin must communicate the Admin Post Key directly to the restricted user for them to post. This is by design — it represents supervised posting access.
---
### Handshake says "One or both keys are incorrect."
- Both participants must submit from their own accounts (you cannot complete both sides from one account).
- Emoji must match exactly — the picker submits codepoints, not descriptions.
- Try clearing both fields and reselecting. The CLEAR button resets the hidden input.
- Verify both participants have submitted independently.
---
## Testing Checklist (Pre-Session)
- [ ] Hotspot enabled before starting server — correct IP appears in banner
- [ ] `/qr` loads a scannable QR code from the server device
- [ ] Join as User 1 from a phone — "Anon #1" assigned, Default Post Key shown on join confirmation
- [ ] Join as User 2 from a second device or incognito tab — "Anon #2" assigned
- [ ] Post from User 1 with correct Default Post Key — appears in feed
- [ ] Post from User 1 with wrong key — "Post key incorrect." error
- [ ] Vote from User 2 — score updates
- [ ] Report from User 2 — spoiler overlay appears, User 1 sees notification banner
- [ ] Admin panel accessible at `/admin` with 4-emoji Admin Post Key
- [ ] Admin restricts User 1 — Default Post Key rotates (new key visible in admin panel)
- [ ] User 1 post fails with old key, succeeds with new key (or admin co-sign)
- [ ] Handshake between User 1 and User 2 — confirmed, username appears on posts for the friend
- [ ] Friends-only toggle shows only friend posts
- [ ] Profile notes edit blocked without Profile Key, succeeds with correct key
- [ ] Hide QR via admin → `/qr` returns 404
- [ ] Flood Attack → 5 bot posts appear in feed
- [ ] Scroll to "WHAT THE PLATFORM SEES" in admin panel — all three panels render (deanonymization, social graph, behavioral correlations)
- [ ] Confirm panels are empty (no crash) on a fresh session with no posts/votes/reports

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -0,0 +1,102 @@
# Meshagora
A social platform simulator for teaching digital literacy. Runs as a local web server on an Android phone (via Termux), participants join through their browsers over WiFi — no internet required, no app install, no persistent data.
Designed for workshop facilitation. Every session starts clean. All data lives in memory and is destroyed when the server stops.
---
## What it teaches
Meshagora recreates the mechanics of real social platforms in a controlled, observable environment:
- Tiered posting access (who gets amplified, who gets restricted)
- Pseudonymity vs. identity (session labels vs. confirmed friends)
- Community reporting and auto-restriction
- Algorithmic feed ordering (invisible vote weights)
- Handshake-based friend networks and friends-only feeds
- Flood attacks (bot accounts)
- Admin surveillance — the platform sees everything participants don't
---
## Requirements
```
Python 3.8+
flask
qrcode[pil]
```
---
## Quick Start — Termux (Android, recommended for sessions)
```bash
pkg install python git
pip install flask qrcode[pil]
git clone https://github.com/JL-Kruger/meshagora.git
cd meshagora
```
1. Enable the Android **mobile hotspot** before running the script.
2. Run:
```bash
python3 meshagora.py
```
3. The startup banner prints the join URL, QR code link, and all post keys.
4. Participants connect to the hotspot WiFi and open the join URL in any browser.
See `DEPLOY.md` for the full pre-session checklist, admin panel guide, and troubleshooting.
---
## Quick Start — Desktop (testing)
```bash
pip install flask qrcode[pil]
python3 meshagora.py
```
Server runs at `http://127.0.0.1:5000`. Open multiple browser tabs or incognito windows to simulate multiple participants.
---
## Architecture
Single-file Flask application (`meshagora.py`). No framework, no database file, no external assets. SQLite runs in-memory. `threaded=False` is mandatory — do not change it.
### Post tiers
| Tier | Key length | Who has it |
|---|---|---|
| ADMIN | 4 emoji | Facilitator only |
| TRUSTED | 2 emoji | Promoted users |
| DEFAULT | 2 emoji | All registered users (rotates on restriction events) |
| RESTRICTED | co-sign | Users restricted by admin or community report |
### Feed ordering
1. Highest-score post pinned to top
2. Middle posts ordered newest-first
3. Lowest-score post pinned to bottom
### Auto-restriction
When unique reporters exceed 51% of a user's total posts, that user is automatically restricted and the Default Post Key rotates silently.
---
## Files
| File | Purpose |
|---|---|
| `meshagora.py` | Complete implementation |
| `meshagora_spec_v2_3.md` | Full design specification |
| `DEPLOY.md` | Deployment guide, troubleshooting, pre-session checklist |
---
## Stopping the Server
`Ctrl+C` in the terminal. All session data is gone immediately. This is intentional.

View File

@@ -0,0 +1,871 @@
Output from termux on first meshagora test
File "/data/data/com.termux/files/usr/tmp/pip-build-env-wg8pkhvc/overlay/lib/python3.12/site-packages/setuptools/build_meta.py", line 317, in run_setup
exec(code, locals())
File "<string>", line 1118, in <module>
RequiredDependencyException:
The headers or library files could not be found for jpeg,
a required dependency when compiling Pillow from source.
Please see the install instructions at:
https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for pillow
Successfully built markupsafe
Failed to build pillow
error: failed-wheel-build-for-install
× Failed to build installable wheels for some pyproject.toml based projects
╰─> pillow
~ $ pkg uodate
Unknown command: 'uodate' (run 'pkg help' for usage information)
~ $ pkg update
No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror:
[] https://linux.domainesia.com/applications/termux/termux-main: ok
Hit:1 https://linux.domainesia.com/applications/termux/termux-main stable InRelease
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
72 packages can be upgraded. Run 'apt list —upgradable' to see them.
~ $ pkg upgrade
No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror:
[ ] https://linux.domainesia.com/applications/termux/termux-main: ok
Hit:1 https://linux.domainesia.com/applications/termux/termux-main stable InRelease
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
72 packages can be upgraded. Run 'apt list —upgradable' to see them.
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
Calculating upgrade… Done
The following packages will be upgraded:
apt bash bzip2 ca-certificates command-not-found
coreutils curl dash debianutils dialog diffutils
dos2unix dpkg ed findutils gawk gpgv grep gzip
inetutils less libandroid-glob libandroid-selinux
libandroid-support libassuan libbz2 libc++
libcap-ng libcurl libevent libgcrypt libgmp
libgnutls libgpg-error libiconv libidn2 liblz4
liblzma libmd libmpfr libnettle libnghttp2
libnghttp3 libnpth libsmartcols libssh2 libtirpc
libunbound libunistring lsof nano net-tools
openssl patch pcre2 procps psmisc readline sed
tar termux-am termux-am-socket termux-core
termux-exec termux-keyring termux-tools unzip
util-linux xxhash xz-utils zlib zstd
72 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 19.9 MB of archives.
After this operation, 2109 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 ca-certificates all 1:2025.11.04 [127 kB]
Get:2 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 zlib aarch64 1.3.1-1 [60.1 kB]
Get:3 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 openssl aarch64 1:3.6.0 [2482 kB]
Get:4 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 pcre2 aarch64 10.47 [961 kB]
Get:5 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libandroid-selinux aarch64 14.0.0.11-1 [59.6 kB]
Get:6 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libiconv aarch64 1.18-1 [561 kB]
Get:7 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libandroid-support aarch64 29-1 [10.9 kB]
Get:8 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libc++ aarch64 29 [335 kB]
Get:9 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libgmp aarch64 6.3.0-2 [328 kB]
Get:10 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 coreutils aarch64 9.9 [778 kB]
Get:11 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libbz2 aarch64 1.0.8-8 [26.1 kB]
Get:12 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 bzip2 aarch64 1.0.8-8 [26.2 kB]
Get:13 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 diffutils aarch64 3.12-2 [163 kB]
Get:14 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 gzip aarch64 1.14-1 [87.5 kB]
Get:15 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 less aarch64 685 [131 kB]
Get:16 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 liblzma aarch64 5.8.1-1 [191 kB]
Get:17 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 xz-utils aarch64 5.8.1-1 [71.1 kB]
Get:18 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libmd aarch64 1.1.0-1 [40.6 kB]
Get:19 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libandroid-glob aarch64 0.6-3 [7032 B]
Get:20 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 tar aarch64 1.35-1 [342 kB]
Get:21 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 zstd aarch64 1.5.7-1 [360 kB]
Get:22 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 dpkg aarch64 1.22.6-5 [308 kB]
Get:23 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 findutils aarch64 4.10.0-1 [251 kB]
Get:24 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libgpg-error aarch64 1.55-1 [120 kB]
Get:25 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libassuan aarch64 3.0.2-1 [73.9 kB]
Get:26 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libgcrypt aarch64 1.11.2-1 [500 kB]
Get:27 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libnpth aarch64 1.6-3 [11.2 kB]
Get:28 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 gpgv aarch64 2.5.11 [187 kB]
Get:29 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 grep aarch64 3.12-2 [128 kB]
Get:30 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libnettle aarch64 3.10.2-1 [406 kB]
Get:31 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libunistring aarch64 1.3-1 [551 kB]
Get:32 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libidn2 aarch64 2.3.8-1 [105 kB]
Get:33 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libevent aarch64 2.1.12-3 [203 kB]
Get:34 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libnghttp2 aarch64 1.68.0 [95.5 kB]
Get:35 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libunbound aarch64 1.24.2 [366 kB]
Get:36 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libgnutls aarch64 3.8.10 [729 kB]
Get:37 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 liblz4 aarch64 1.10.0-1 [84.7 kB]
Get:38 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 sed aarch64 4.9-2 [118 kB]
Get:39 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libnghttp3 aarch64 1.13.1 [67.4 kB]
Get:40 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libssh2 aarch64 1.11.1-1 [218 kB]
Get:41 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libcurl aarch64 8.17.0 [990 kB]
Get:42 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 curl aarch64 8.17.0 [236 kB]
Get:43 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 dash aarch64 0.5.12-1 [65.8 kB]
Get:44 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libmpfr aarch64 4.2.1-1 [272 kB]
Get:45 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 readline aarch64 8.3.1-2 [294 kB]
Get:46 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 gawk aarch64 5.3.1-2 [782 kB]
Get:47 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 procps aarch64 3.3.17-6 [143 kB]
Get:48 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 psmisc aarch64 23.7-1 [41.2 kB]
Get:49 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-am all 0.8.0-2 [577 kB]
Get:50 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-am-socket aarch64 1.5.0-1 [16.1 kB]
Get:51 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-core aarch64 0.4.0-1 [198 kB]
Get:52 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-exec aarch64 1:2.4.0-1 [288 kB]
Get:53 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libsmartcols aarch64 2.41.2 [99.8 kB]
Get:54 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libcap-ng aarch64 2:0.8.5-1 [35.7 kB]
Get:55 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 util-linux aarch64 2.41.2 [738 kB]
Get:56 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 dialog aarch64 1.3-20240307-1 [100 kB]
Get:57 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-tools aarch64 1.46.0+really1.45.0-1 [33.6 kB]
Get:58 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 termux-keyring all 3.13 [39.8 kB]
Get:59 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 xxhash aarch64 0.8.3-1 [76.6 kB]
Get:60 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 apt aarch64 2.8.1-2 [1031 kB]
Get:61 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 bash aarch64 5.3.3-1 [956 kB]
Get:62 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 command-not-found aarch64 3.2-6 [106 kB]
Get:63 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 debianutils aarch64 5.23.2-1 [16.8 kB]
Get:64 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 dos2unix aarch64 7.5.2-1 [64.4 kB]
Get:65 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 ed aarch64 1.22.3 [42.8 kB]
Get:66 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 inetutils aarch64 2.6-1 [223 kB]
Get:67 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 libtirpc aarch64 1.3.7-1 [124 kB]
Get:68 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 lsof aarch64 4.99.5-2 [122 kB]
Get:69 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 nano aarch64 8.7 [230 kB]
Get:70 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 net-tools aarch64 2.10.0-1 [118 kB]
Get:71 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 patch aarch64 2.8-1 [74.0 kB]
Get:72 https://linux.domainesia.com/applications/termux/termux-main stable/main aarch64 unzip aarch64 6.0-10 [117 kB]
Fetched 19.9 MB in 1min 20s (249 kB/s)
(Reading database … 17345 files and directories currently installed.)
Preparing to unpack …/ca-certificates_1%3a2025.11.04_all.deb …
Unpacking ca-certificates (1:2025.11.04) over (1:2025.02.25) …
Setting up ca-certificates (1:2025.11.04) …
(Reading database … 17345 files and directories currently installed.)
Preparing to unpack …/zlib_1.3.1-1_aarch64.deb …
Unpacking zlib (1.3.1-1) over (1.3.1) …
Setting up zlib (1.3.1-1) …
(Reading database … 17345 files and directories currently installed.)
Preparing to unpack …/openssl_1%3a3.6.0_aarch64.deb …
Unpacking openssl (1:3.6.0) over (1:3.4.1) …
Setting up openssl (1:3.6.0) …
Configuration file '/data/data/com.termux/files/usr/etc/tls/openssl.cnf'
⟹ File on system created by you or by a script.
⟹ File also in package provided by package maintainer.
What would you like to do about it ? Your options are:
Y or I : install the package maintainer's version
N or O : keep your currently-installed version
D : show the differences between the versions
Z : start a shell to examine the situation
The default action is to keep your current version.
*** openssl.cnf (Y/I/N/O/D/Z) [default=N] ? y
Installing new version of config file /data/data/com.termux/files/usr/etc/tls/openssl.cnf …
(Reading database … 17346 files and directories currently installed.)
Preparing to unpack …/pcre2_10.47_aarch64.deb …
Unpacking pcre2 (10.47) over (10.45) …
Setting up pcre2 (10.47) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/libandroid-selinux_14.0.0.11-1_aarch64.deb …
Unpacking libandroid-selinux (14.0.0.11-1) over (14.0.0.11) …
Setting up libandroid-selinux (14.0.0.11-1) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/libiconv_1.18-1_aarch64.deb …
Unpacking libiconv (1.18-1) over (1.18) …
Setting up libiconv (1.18-1) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/libandroid-support_29-1_aarch64.deb …
Unpacking libandroid-support (29-1) over (29) …
Setting up libandroid-support (29-1) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/archives/libc++_29_aarch64.deb …
Unpacking libc++ (29) over (27c) …
Setting up libc++ (29) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/libgmp_6.3.0-2_aarch64.deb …
Unpacking libgmp (6.3.0-2) over (6.3.0-1) …
Setting up libgmp (6.3.0-2) …
(Reading database … 17348 files and directories currently installed.)
Preparing to unpack …/coreutils_9.9_aarch64.deb …
Unpacking coreutils (9.9) over (9.6-1) …
Setting up coreutils (9.9) …
update-alternatives: using /data/data/com.termux/files/usr/libexec/coreutils/cat to provide /data/data/com.termux/files/usr/bin/pager (pager) in auto mode
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libbz2_1.0.8-8_aarch64.deb …
Unpacking libbz2 (1.0.8-8) over (1.0.8-6) …
Setting up libbz2 (1.0.8-8) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/bzip2_1.0.8-8_aarch64.deb …
Unpacking bzip2 (1.0.8-8) over (1.0.8-6) …
Setting up bzip2 (1.0.8-8) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/diffutils_3.12-2_aarch64.deb …
Unpacking diffutils (3.12-2) over (3.11) …
Setting up diffutils (3.12-2) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/gzip_1.14-1_aarch64.deb …
Unpacking gzip (1.14-1) over (1.13) …
Setting up gzip (1.14-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/archives/less_685_aarch64.deb …
Unpacking less (685) over (668) …
Setting up less (685) …
update-alternatives: using /data/data/com.termux/files/usr/bin/less to provide /data/data/com.termux/files/usr/bin/pager (pager) in auto mode
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/liblzma_5.8.1-1_aarch64.deb …
Unpacking liblzma (5.8.1-1) over (5.8.0) …
Setting up liblzma (5.8.1-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/xz-utils_5.8.1-1_aarch64.deb …
Unpacking xz-utils (5.8.1-1) over (5.8.0) …
Setting up xz-utils (5.8.1-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libmd_1.1.0-1_aarch64.deb …
Unpacking libmd (1.1.0-1) over (1.1.0) …
Setting up libmd (1.1.0-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libandroid-glob_0.6-3_aarch64.deb …
Unpacking libandroid-glob (0.6-3) over (0.6-2) …
Setting up libandroid-glob (0.6-3) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/tar_1.35-1_aarch64.deb …
Unpacking tar (1.35-1) over (1.35) …
Setting up tar (1.35-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/zstd_1.5.7-1_aarch64.deb …
Unpacking zstd (1.5.7-1) over (1.5.7) …
Setting up zstd (1.5.7-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/dpkg_1.22.6-5_aarch64.deb …
Unpacking dpkg (1.22.6-5) over (1.22.6-1) …
Setting up dpkg (1.22.6-5) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/findutils_4.10.0-1_aarch64.deb …
Unpacking findutils (4.10.0-1) over (4.10.0) …
Setting up findutils (4.10.0-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libgpg-error_1.55-1_aarch64.deb …
Unpacking libgpg-error (1.55-1) over (1.50) …
Setting up libgpg-error (1.55-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libassuan_3.0.2-1_aarch64.deb …
Unpacking libassuan (3.0.2-1) over (3.0.1-2) …
Setting up libassuan (3.0.2-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libgcrypt_1.11.2-1_aarch64.deb …
Unpacking libgcrypt (1.11.2-1) over (1.11.0) …
Setting up libgcrypt (1.11.2-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libnpth_1.6-3_aarch64.deb …
Unpacking libnpth (1.6-3) over (1.6-2) …
Setting up libnpth (1.6-3) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/gpgv_2.5.11_aarch64.deb …
Unpacking gpgv (2.5.11) over (2.4.5-3) …
Setting up gpgv (2.5.11) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/grep_3.12-2_aarch64.deb …
Unpacking grep (3.12-2) over (3.11) …
Setting up grep (3.12-2) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libnettle_3.10.2-1_aarch64.deb …
Unpacking libnettle (3.10.2-1) over (3.10.1) …
Setting up libnettle (3.10.2-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libunistring_1.3-1_aarch64.deb …
Unpacking libunistring (1.3-1) over (1.3) …
Setting up libunistring (1.3-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libidn2_2.3.8-1_aarch64.deb …
Unpacking libidn2 (2.3.8-1) over (2.3.7) …
Setting up libidn2 (2.3.8-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libevent_2.1.12-3_aarch64.deb …
Unpacking libevent (2.1.12-3) over (2.1.12-2) …
Setting up libevent (2.1.12-3) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libnghttp2_1.68.0_aarch64.deb …
Unpacking libnghttp2 (1.68.0) over (1.65.0) …
Setting up libnghttp2 (1.68.0) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libunbound_1.24.2_aarch64.deb …
Unpacking libunbound (1.24.2) over (1.22.0) …
Setting up libunbound (1.24.2) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libgnutls_3.8.10_aarch64.deb …
Unpacking libgnutls (3.8.10) over (3.8.9) …
Setting up libgnutls (3.8.10) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/liblz4_1.10.0-1_aarch64.deb …
Unpacking liblz4 (1.10.0-1) over (1.10.0) …
Setting up liblz4 (1.10.0-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/archives/sed_4.9-2_aarch64.deb …
Unpacking sed (4.9-2) over (4.9-1) …
Setting up sed (4.9-2) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libnghttp3_1.13.1_aarch64.deb …
Unpacking libnghttp3 (1.13.1) over (1.8.0) …
Setting up libnghttp3 (1.13.1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libssh2_1.11.1-1_aarch64.deb …
Unpacking libssh2 (1.11.1-1) over (1.11.1) …
Setting up libssh2 (1.11.1-1) …
(Reading database … 17349 files and directories currently installed.)
Preparing to unpack …/libcurl_8.17.0_aarch64.deb …
Unpacking libcurl (8.17.0) over (8.12.1) …
Setting up libcurl (8.17.0) …
(Reading database … 17365 files and directories currently installed.)
Preparing to unpack …/curl_8.17.0_aarch64.deb …
Unpacking curl (8.17.0) over (8.12.1) …
Setting up curl (8.17.0) …
(Reading database … 17365 files and directories currently installed.)
Preparing to unpack …/dash_0.5.12-1_aarch64.deb …
Unpacking dash (0.5.12-1) over (0.5.12) …
Setting up dash (0.5.12-1) …
(Reading database … 17365 files and directories currently installed.)
Preparing to unpack …/libmpfr_4.2.1-1_aarch64.deb …
Unpacking libmpfr (4.2.1-1) over (4.2.1) …
Setting up libmpfr (4.2.1-1) …
(Reading database … 17365 files and directories currently installed.)
Preparing to unpack …/readline_8.3.1-2_aarch64.deb …
Unpacking readline (8.3.1-2) over (8.2.13) …
Setting up readline (8.3.1-2) …
(Reading database … 17370 files and directories currently installed.)
Preparing to unpack …/gawk_5.3.1-2_aarch64.deb …
Unpacking gawk (5.3.1-2) over (5.3.0) …
Setting up gawk (5.3.1-2) …
(Reading database … 17368 files and directories currently installed.)
Preparing to unpack …/procps_3.3.17-6_aarch64.deb …
Unpacking procps (3.3.17-6) over (3.3.17-5) …
Setting up procps (3.3.17-6) …
(Reading database … 17368 files and directories currently installed.)
Preparing to unpack …/psmisc_23.7-1_aarch64.deb …
Unpacking psmisc (23.7-1) over (23.7) …
Setting up psmisc (23.7-1) …
(Reading database … 17368 files and directories currently installed.)
Preparing to unpack …/termux-am_0.8.0-2_all.deb …
Unpacking termux-am (0.8.0-2) over (0.8.0-1) …
Setting up termux-am (0.8.0-2) …
(Reading database … 17368 files and directories currently installed.)
Preparing to unpack …/termux-am-socket_1.5.0-1_aarch64.deb …
Unpacking termux-am-socket (1.5.0-1) over (1.5.0) …
Setting up termux-am-socket (1.5.0-1) …
(Reading database … 17368 files and directories currently installed.)
Preparing to unpack …/termux-core_0.4.0-1_aarch64.deb …
Unpacking termux-core (0.4.0-1) over (0.3.0) …
Setting up termux-core (0.4.0-1) …
(Reading database … 17369 files and directories currently installed.)
Preparing to unpack …/termux-exec_1%3a2.4.0-1_aarch64.deb …
Unpacking termux-exec (1:2.4.0-1) over (1:2.3.0) …
Setting up termux-exec (1:2.4.0-1) …
termux-exec.postinst: Start
termux-exec.postinst: android_build_version_sdk: '36'
termux-exec: Setting primary Termux '$LD_PRELOAD' library in 'libtermux-exec-ld-preload.so' to '/data/data/com.termux/files/usr/lib/libtermux-exec-direct-ld-preload.so'
termux-exec.postinst: End
(Reading database … 17372 files and directories currently installed.)
Preparing to unpack …/libsmartcols_2.41.2_aarch64.deb …
Unpacking libsmartcols (2.41.2) over (2.40.2-3) …
Setting up libsmartcols (2.41.2) …
(Reading database … 17372 files and directories currently installed.)
Preparing to unpack …/libcap-ng_2%3a0.8.5-1_aarch64.deb …
Unpacking libcap-ng (2:0.8.5-1) over (2:0.8.5) …
Setting up libcap-ng (2:0.8.5-1) …
(Reading database … 17372 files and directories currently installed.)
Preparing to unpack …/util-linux_2.41.2_aarch64.deb …
Unpacking util-linux (2.41.2) over (2.40.2-3) …
Setting up util-linux (2.41.2) …
(Reading database … 17378 files and directories currently installed.)
Preparing to unpack …/dialog_1.3-20240307-1_aarch64.deb …
Unpacking dialog (1.3-20240307-1) over (1.3-20240307-0) …
Setting up dialog (1.3-20240307-1) …
(Reading database … 17378 files and directories currently installed.)
Preparing to unpack …/termux-tools_1.46.0+really1.45.0-1_aarch64.deb …
Unpacking termux-tools (1.46.0+really1.45.0-1) over (1.45.0) …
Setting up termux-tools (1.46.0+really1.45.0-1) …
(Reading database … 17378 files and directories currently installed.)
Preparing to unpack …/termux-keyring_3.13_all.deb …
Unpacking termux-keyring (3.13) over (3.12-1) …
Setting up termux-keyring (3.13) …
(Reading database … 17381 files and directories currently installed.)
Preparing to unpack …/xxhash_0.8.3-1_aarch64.deb …
Unpacking xxhash (0.8.3-1) over (0.8.3) …
Setting up xxhash (0.8.3-1) …
(Reading database … 17381 files and directories currently installed.)
Preparing to unpack …/apt_2.8.1-2_aarch64.deb …
Unpacking apt (2.8.1-2) over (2.8.1-1) …
Setting up apt (2.8.1-2) …
Configuration file '/data/data/com.termux/files/usr/etc/apt/sources.list'
⟹ File on system created by you or by a script.
⟹ File also in package provided by package maintainer.
What would you like to do about it ? Your options are:
Y or I : install the package maintainer's version
N or O : keep your currently-installed version
D : show the differences between the versions
Z : start a shell to examine the situation
The default action is to keep your current version.
*** sources.list (Y/I/N/O/D/Z) [default=N] ? y
Installing new version of config file /data/data/com.termux/files/usr/etc/apt/sources.list …
(Reading database … 17381 files and directories currently installed.)
Preparing to unpack …/bash_5.3.3-1_aarch64.deb …
Unpacking bash (5.3.3-1) over (5.2.37-2) …
Setting up bash (5.3.3-1) …
Configuration file '/data/data/com.termux/files/usr/etc/bash.bashrc'
⟹ File on system created by you or by a script.
⟹ File also in package provided by package maintainer.
What would you like to do about it ? Your options are:
Y or I : install the package maintainer's version
N or O : keep your currently-installed version
D : show the differences between the versions
Z : start a shell to examine the situation
The default action is to keep your current version.
*** bash.bashrc (Y/I/N/O/D/Z) [default=N] ? y
Installing new version of config file /data/data/com.termux/files/usr/etc/bash.bashrc …
(Reading database … 17388 files and directories currently installed.)
Preparing to unpack …/00-command-not-found_3.2-6_aarch64.deb …
Unpacking command-not-found (3.2-6) over (2.4.0-68) …
Preparing to unpack …/01-debianutils_5.23.2-1_aarch64.deb …
Unpacking debianutils (5.23.2-1) over (5.21) …
Preparing to unpack …/02-dos2unix_7.5.2-1_aarch64.deb …
Unpacking dos2unix (7.5.2-1) over (7.5.2) …
Preparing to unpack …/03-ed_1.22.3_aarch64.deb …
Unpacking ed (1.22.3) over (1.21.1) …
Preparing to unpack …/04-inetutils_2.6-1_aarch64.deb …
Unpacking inetutils (2.6-1) over (2.6) …
Preparing to unpack …/05-libtirpc_1.3.7-1_aarch64.deb …
Unpacking libtirpc (1.3.7-1) over (1.3.6) …
Preparing to unpack …/06-lsof_4.99.5-2_aarch64.deb …
Unpacking lsof (4.99.5-2) over (4.99.4) …
Preparing to unpack …/07-nano_8.7_aarch64.deb …
Unpacking nano (8.7) over (8.3) …
Preparing to unpack …/08-net-tools_2.10.0-1_aarch64.deb …
Unpacking net-tools (2.10.0-1) over (2.10.0) …
Preparing to unpack …/09-patch_2.8-1_aarch64.deb …
Unpacking patch (2.8-1) over (2.7.6-4) …
Preparing to unpack …/10-unzip_6.0-10_aarch64.deb …
Unpacking unzip (6.0-10) over (6.0-9) …
Setting up libtirpc (1.3.7-1) …
Setting up net-tools (2.10.0-1) …
Setting up inetutils (2.6-1) …
Setting up unzip (6.0-10) …
Setting up ed (1.22.3) …
Setting up command-not-found (3.2-6) …
Setting up patch (2.8-1) …
Setting up lsof (4.99.5-2) …
Setting up nano (8.7) …
update-alternatives: updating alternative /data/data/com.termux/files/usr/bin/nano because link group editor has changed slave links
update-alternatives: warning: skipping updating manpage database as 'makewhatis' command from 'mandoc' package is not installed
Setting up debianutils (5.23.2-1) …
Setting up dos2unix (7.5.2-1) …
~ $ pkg install libjpeg-turbo libpng freetype libwebppython-dev clang
No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror:
[] https://packages-cf.termux.dev/apt/termux-main/: ok
Get:1 https://packages-cf.termux.dev/apt/termux-main stable InRelease [14.0 kB]
Get:2 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 Packages [550 kB]
Fetched 564 kB in 5s (113 kB/s)
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
47 packages can be upgraded. Run 'apt list —upgradable' to see them.
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
E: Unable to locate package libwebppython-dev
~ $ pkg install libwebp python-dev
No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror:
[ ] https://packages-cf.termux.dev/apt/termux-main/: ok
Reading package lists… Done
Building dependency tree… Done
Reading state information… Done
Package python-dev is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
However the following packages replace it:
python
E: Package 'python-dev' has no installation candidate
~ $ pip install flask qrcode[pil]
Collecting flask
Using cached flask-3.1.3-py3-none-any.whl.metadata (3.2 kB)
Collecting qrcode[pil]
Using cached qrcode-8.2-py3-none-any.whl.metadata (17 kB)
Collecting blinker≥1.9.0 (from flask)
Using cached blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting click≥8.1.3 (from flask)
Using cached click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous≥2.2.0 (from flask)
Using cached itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting jinja2≥3.1.2 (from flask)
Using cached jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting markupsafe≥2.1.1 (from flask)
Using cached markupsafe-3.0.3-cp312-cp312-linux_aarch64.whl
Collecting werkzeug≥3.1.0 (from flask)
Using cached werkzeug-3.1.6-py3-none-any.whl.metadata (4.0 kB)
Collecting pillow≥9.1.0 (from qrcode[pil])
Using cached pillow-12.1.1.tar.gz (47.0 MB)
Installing build dependencies … done
Getting requirements to build wheel … done
Preparing metadata (pyproject.toml) … done
Using cached flask-3.1.3-py3-none-any.whl (103 kB)
Using cached qrcode-8.2-py3-none-any.whl (45 kB)
Using cached blinker-1.9.0-py3-none-any.whl (8.5 kB)
Using cached click-8.3.1-py3-none-any.whl (108 kB)
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Using cached jinja2-3.1.6-py3-none-any.whl (134 kB)
Using cached werkzeug-3.1.6-py3-none-any.whl (225 kB)
Building wheels for collected packages: pillow
Building wheel for pillow (pyproject.toml) … error
error: subprocess-exited-with-error
× Building wheel for pillow (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [235 lines of output]
running bdist_wheel
running build
running build_py
creating build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/AvifImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/BdfFontFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/BlpImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/BmpImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/BufrStubImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ContainerIO.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/CurImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/DcxImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/DdsImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/EpsImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ExifTags.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/FitsImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/FliImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/FontFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/FpxImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/FtexImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GbrImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GdImageFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GifImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GimpGradientFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GimpPaletteFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/GribStubImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/Hdf5StubImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/IcnsImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/IcoImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/Image.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageChops.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageCms.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageColor.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageDraw.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageDraw2.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageEnhance.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageFilter.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageFont.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageGrab.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageMath.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageMode.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageMorph.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageOps.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImagePalette.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImagePath.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageQt.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageSequence.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageShow.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageStat.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageText.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageTk.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageTransform.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImageWin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/ImtImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/IptcImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/Jpeg2KImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/JpegImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/JpegPresets.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/McIdasImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/MicImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/MpegImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/MpoImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/MspImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PSDraw.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PaletteFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PalmImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PcdImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PcfFontFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PcxImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PdfImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PdfParser.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PixarImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/TarIO.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PngImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PpmImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/PsdImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/QoiImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/SgiImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/SpiderImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/SunImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/TgaImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/TiffImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/TiffTags.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/WalImageFile.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/WebPImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/WmfImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/XVThumbImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/XbmImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/XpmImagePlugin.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/__init__.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/__main__.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_binary.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_deprecate.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_tkinter_finder.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_typing.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_util.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_version.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/features.py → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/report.py → build/lib.linux-aarch64-cpython-312/PIL
running egg_info
writing src/pillow.egg-info/PKG-INFO
writing dependency_links to src/pillow.egg-info/dependency_links.txt
writing requirements to src/pillow.egg-info/requires.txt
writing top-level names to src/pillow.egg-info/top_level.txt
reading manifest file 'src/pillow.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
warning: no files found matching '*.c'
warning: no files found matching '*.h'
warning: no files found matching '*.sh'
warning: no files found matching '*.txt'
warning: no files found matching '.flake8'
warning: no previously-included files found matching '.clang-format'
warning: no previously-included files found matching '.coveragerc'
warning: no previously-included files found matching '.editorconfig'
warning: no previously-included files found matching '.readthedocs.yml'
warning: no previously-included files found matching 'codecov.yml'
warning: no previously-included files found matching 'renovate.json'
warning: no previously-included files found matching 'Tests/images/README.md'
warning: no previously-included files found matching 'Tests/images/crash*.tif'
warning: no previously-included files found matching 'Tests/images/string_dimension.tiff'
warning: no previously-included files matching '.git*' found anywhere in distribution
warning: no previously-included files matching '*.so' found anywhere in distribution
no previously-included directories found matching '.ci' no previously-included directories found matching 'wheels'
no previously-included directories found matching 'winbuild/build'
no previously-included directories found matching 'winbuild/depends'
no previously-included directories found matching 'Tests/errors' no previously-included directories found matching 'Tests/images/jpeg2000' no previously-included directories found matching 'Tests/images/msp' no previously-included directories found matching 'Tests/images/picins' no previously-included directories found matching 'Tests/images/sunraster' no previously-included directories found matching 'Tests/test-images' adding license file 'LICENSE'
writing manifest file 'src/pillow.egg-info/SOURCES.txt'
copying src/PIL/_avif.pyi → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_imaging.pyi → build/lib.linux-aarch64-cpython-312/PIL
copying src/PIL/_imagingcms.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/_imagingft.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/_imagingmath.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/_imagingmorph.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/_imagingtk.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/_webp.pyi → build/lib.linux-aarch64-cpython-312/PIL copying src/PIL/py.typed → build/lib.linux-aarch64-cpython-312/PIL running build_clib
building 'pil_imaging_mode' library creating build/temp.linux-aarch64-cpython-312/src/libImaging aarch64-linux-android-clang -fno-strict-overflow -Wsign-compare -Wunreachable-code -DNDEBUG -g -O3 -Wall -fstack-protector-strong -O3 -fstack-protector-strong -O3 -fPIC -c src/libImaging/Mode.c -o build/temp.linux-aarch64-cpython-312/src/libImaging/Mode.o
llvm-ar rcs build/temp.linux-aarch64-cpython-312/libpil_imaging_mode.a build/temp.linux-aarch64-cpython-312/src/libImaging/Mode.o running build_ext
The headers or library files could not be found for jpeg,
a required dependency when compiling Pillow from source.
Please see the install instructions at:
https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
Traceback (most recent call last):
File "<string>", line 1101, in <module> File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/__init__.py", line 117, in setup
return distutils.core.setup(attrs) # type: ignore[return-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/core.py", line 186, in setup
return run_commands(dist) ^^^^^^^^^^^^^^^^^^
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/core.py", line 202, in run_commands dist.run_commands()
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 1000, in run_commands
self.run_command(cmd)
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/dist.py", line 1107, in run_command
super().run_command(command)
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 1019, in run_command
cmd_obj.run() File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/command/bdist_wheel.py", line 370, in run self.run_command("build")
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/cmd.py", line 341, in run_command
self.distribution.run_command(command) File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/dist.py", line 1107, in run_command
super().run_command(command)
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 1019, in run_command cmd_obj.run()
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/command/build.py", line 135, in run
self.run_command(cmd_name) File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/cmd.py", line 341, in run_command self.distribution.run_command(command)
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/dist.py", line 1107, in run_command
super().run_command(command)
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/dist.py", line 1019, in run_command
cmd_obj.run() File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/command/build_ext.py", line 97, in run
build ext.run(self) File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/_distutils/command/build_ext.py", line 367, in run self.build_extensions()
File "<string>", line 897, in build_extensions
RequiredDependencyException: jpeg
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/data/data/com.termux/files/usr/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 389, in <module>
main()
File "/data/data/com.termux/files/usr/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 373, in main
json_out["return_val"] = hook( hook_input["kwargs"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/data/data/com.termux/files/usr/lib/python3.12/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 280, in build_wheel
return build backend().build_wheel(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/data/com.termux/files/usr/tmp/pip-install-6uux7pi6/pillow_094bbceb25664f5f96eceee9372926bb/_custom_build/backend.py", line 26, in build_wheel
return super().build_wheel(wheel_directory, config_settings, metadata_directory) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/build_meta.py", line 441, in build_wheel return build(['bdist wheel', '—dist-info-dir', str(metadata_directory)])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/build_meta.py", line 429, in build return self. build_with_temp_dir(
^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/build_meta.py", line 410, in build with_temp_dir
self.run_setup() File "/data/data/com.termux/files/usr/tmp/pip-install-6uux7pi6/pillow_094bbceb25664f5f96eceee9372926bb/_custom_build/backend.py", line 20, in run_setup return super().run_setup(setup_script) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/data/data/com.termux/files/usr/tmp/pip-build-env-pjh12tjv/overlay/lib/python3.12/site-packages/setuptools/build_meta.py", line 317, in run_setup
exec(code, locals()) File "<string>", line 1118, in <module> RequiredDependencyException:
The headers or library files could not be found for jpeg,
a required dependency when compiling Pillow from source.
Please see the install instructions at: https://pillow.readthedocs.io/en/latest/installation/basic-installation.html
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for pillow Failed to build pillow error: failed-wheel-build-for-install
× Failed to build installable wheels for some pyproject.toml based projects
╰─> pillow ~ $ pkg install libjpeg-turbo libpng python-dev clang pkg-config freetype libwebp
No mirror or mirror group selected. You might want to select one by running 'termux-change-repo' Checking availability of current mirror:
[] https://packages-cf.termux.dev/apt/termux-main/: ok Reading package lists… Done
Building dependency tree… Done Reading state information… Done Package python-dev is not available, but is referred to by another package. This may mean that the package is missing, has been obsoleted, or
is only available from another source However the following packages replace it: python
E: Package 'python-dev' has no installation candidate~ $ pkg install libjpeg-turbo libpng clang pkg-config freetype libwebp No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror: [ ] https://packages-cf.termux.dev/apt/termux-main/: ok
Reading package lists… Done Building dependency tree… Done Reading state information… Done
pkg-config is already the newest version (0.29.2-3). pkg-config set to manually installed. The following additional packages will be installed:
brotli giflib libllvm libtiff lld llvm The following NEW packages will be installed: brotli freetype giflib libjpeg-turbo libpng
libtiff libwebp The following packages will be upgraded: clang libllvm lld llvm
4 upgraded, 7 newly installed, 0 to remove and 43 not upgraded. Need to get 81.0 MB of archives.
After this operation, 18.6 MB of additional disk space will be used. Do you want to continue? [Y/n] y
Get:1 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 brotli aarch64 1.2.0 [341 kB] Get:2 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 llvm aarch64 21.1.8-2 [14.8 MB] Get:3 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 clang aarch64 21.1.8-2 [30.4 MB]
38% [3 clang 19.8 MB/30.4 MB 65%] 158 kB/s 4miGet:4 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 lld aarch64 21.1.8-2 [2814 kB]
Get:5 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libllvm aarch64 21.1.8-2 [30.4 MB]
Get:6 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libpng aarch64 1.6.55 [195 kB] Get:7 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 freetype aarch64 2.14.2 [412 kB]
Get:8 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 giflib aarch64 5.2.2-1 [18.2 kB]
Get:9 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libjpeg-turbo aarch64 3.1.3 [383 kB] Get:10 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libtiff aarch64 4.7.1 [848 kB]
Get:11 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libwebp aarch64 1.6.0-rc1-0 [329 kB] Fetched 81.0 MB in 4min 29s (301 kB/s)
Selecting previously unselected package brotli. (Reading database … 17388 files and directories currently installed.)
Preparing to unpack …/00-brotli_1.2.0_aarch64.deb …
Unpacking brotli (1.2.0) …
Preparing to unpack …/01-llvm_21.1.8-2_aarch64.deb … Unpacking llvm (21.1.8-2) over (21.1.6) …
Preparing to unpack …/02-clang_21.1.8-2_aarch64.deb …
Unpacking clang (21.1.8-2) over (21.1.6) … Preparing to unpack …/03-lld_21.1.8-2_aarch64.deb …
Unpacking lld (21.1.8-2) over (21.1.6) …
Preparing to unpack …/04-libllvm_21.1.8-2_aarch64.deb …
Unpacking libllvm (21.1.8-2) over (21.1.6) …
Selecting previously unselected package libpng.
Preparing to unpack …/05-libpng_1.6.55_aarch64.deb …
Unpacking libpng (1.6.55) …
Selecting previously unselected package freetype. Preparing to unpack …/06-freetype_2.14.2_aarch64.deb …
Unpacking freetype (2.14.2) …
Selecting previously unselected package giflib. Preparing to unpack …/07-giflib_5.2.2-1_aarch64.deb …
Unpacking giflib (5.2.2-1) …
Selecting previously unselected package libjpeg-turbo.
Preparing to unpack …/08-libjpeg-turbo_3.1.3_aarch64.deb …
Unpacking libjpeg-turbo (3.1.3) … Selecting previously unselected package libtiff.
Preparing to unpack …/09-libtiff_4.7.1_aarch64.deb … Unpacking libtiff (4.7.1) …
Selecting previously unselected package libwebp. Preparing to unpack …/10-libwebp_1.6.0-rc1-0_aarch64.deb …
Unpacking libwebp (1.6.0-rc1-0) …
Setting up libpng (1.6.55) … Setting up libjpeg-turbo (3.1.3) …
Setting up libllvm (21.1.8-2) …
Setting up giflib (5.2.2-1) …
Setting up brotli (1.2.0) … Setting up lld (21.1.8-2) …
Setting up libtiff (4.7.1) …
Setting up freetype (2.14.2) …
Setting up llvm (21.1.8-2) … Setting up libwebp (1.6.0-rc1-0) …
Setting up clang (21.1.8-2) …
~ $ pip install flask qrcode[pil] Collecting flask
Using cached flask-3.1.3-py3-none-any.whl.metadata (3.2 kB) Collecting qrcode[pil]
Using cached qrcode-8.2-py3-none-any.whl.metadata (17 kB)
Collecting blinker≥1.9.0 (from flask) Using cached blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
Collecting click≥8.1.3 (from flask)
Using cached click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
Collecting itsdangerous≥2.2.0 (from flask)
Using cached itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB) Collecting jinja2≥3.1.2 (from flask)
Using cached jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
Collecting markupsafe≥2.1.1 (from flask) Using cached markupsafe-3.0.3-cp312-cp312-linux_aarch64.whl
Collecting werkzeug≥3.1.0 (from flask) Using cached werkzeug-3.1.6-py3-none-any.whl.metadata (4.0 kB) Collecting pillow≥9.1.0 (from qrcode[pil])
Using cached pillow-12.1.1.tar.gz (47.0 MB)
Installing build dependencies … done
Getting requirements to build wheel … done Preparing metadata (pyproject.toml) … done
Using cached flask-3.1.3-py3-none-any.whl (103 kB)
Using cached qrcode-8.2-py3-none-any.whl (45 kB)
Using cached blinker-1.9.0-py3-none-any.whl (8.5 kB)
Using cached click-8.3.1-py3-none-any.whl (108 kB)
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Using cached jinja2-3.1.6-py3-none-any.whl (134 kB)
Using cached werkzeug-3.1.6-py3-none-any.whl (225 kB)
Building wheels for collected packages: pillow
Building wheel for pillow (pyproject.toml) … done
Created wheel for pillow: filename=pillow-12.1.1-cp312-cp312-linux_aarch64.whl size=1200433 sha256=bd8fd34796d4b131f670c1e8fcc2de7b7124b824d20f09f26f55dbd903067bdb
Stored in directory: /data/data/com.termux/files/home/.cache/pip/wheels/05/f2/ae/60eba7108f4635d4ebefe50da1192219c87f17d63b67cfb9b9
Successfully built pillow
Installing collected packages: qrcode, pillow, markupsafe, itsdangerous, click, blinker, werkzeug, jinja2, flask
Successfully installed blinker-1.9.0 click-8.3.1 flask-3.1.3 itsdangerous-2.2.0 jinja2-3.1.6 markupsafe-3.0.3 pillow-12.1.1 qrcode-8.2 werkzeug-3.1.6
~ $ mkdir meshagora_prototype && cd meshagora_prototype
~/meshagora_prototype $ git clone https://github.com/JL-Kruger/meshagora.git
The program git is not installed. Install it by executing:
pkg install git
~/meshagora_prototype $ pkg install git No mirror or mirror group selected. You might want to select one by running 'termux-change-repo'
Checking availability of current mirror: [] https://packages-cf.termux.dev/apt/termux-main/: ok
Reading package lists… Done Building dependency tree… Done
Reading state information… Done
The following additional packages will be installed: krb5 ldns libdb libedit libresolv-wrapper openssh
openssh-sftp-server termux-auth
Suggested packages: perl termux-services
The following NEW packages will be installed:
git krb5 ldns libdb libedit libresolv-wrapper
openssh openssh-sftp-server termux-auth
0 upgraded, 9 newly installed, 0 to remove and 43 not upgraded.
Need to get 7440 kB of archives.
After this operation, 45.0 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 git aarch64 2.53.0 [4639 kB] Get:2 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libresolv-wrapper aarch64 1.1.7-6 [11.5 kB]
Get:3 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libdb aarch64 18.1.40-5 [509 kB]
Get:4 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 krb5 aarch64 1.22.2 [902 kB]
Get:5 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 ldns aarch64 1.8.4-1 [303 kB]
Get:6 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 libedit aarch64 20240517-3.1-1 [79.1 kB]
Get:7 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 openssh-sftp-server aarch64 10.2p1-1 [55.4 kB]
Get:8 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 termux-auth aarch64 1.5.0-1 [6996 B] Get:9 https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 openssh aarch64 10.2p1-1 [934 kB]Fetched 7440 kB in 5s (1551 kB/s)
Selecting previously unselected package git. (Reading database … 18003 files and directories currently installed.) Preparing to unpack …/0-git_2.53.0_aarch64.deb …
Unpacking git (2.53.0) …
Selecting previously unselected package libresolv-wrapper. Preparing to unpack …/1-libresolv-wrapper_1.1.7-6_aarch64.deb …
Unpacking libresolv-wrapper (1.1.7-6) …
Selecting previously unselected package libdb.
Preparing to unpack …/2-libdb_18.1.40-5_aarch64.deb …
Unpacking libdb (18.1.40-5) … Selecting previously unselected package krb5.
Preparing to unpack …/3-krb5_1.22.2_aarch64.deb …Unpacking krb5 (1.22.2) …
Selecting previously unselected package ldns. Preparing to unpack …/4-ldns_1.8.4-1_aarch64.deb … Unpacking ldns (1.8.4-1) …
Selecting previously unselected package libedit. Preparing to unpack …/5-libedit_20240517-3.1-1_aarch64.deb … Unpacking libedit (20240517-3.1-1) …
Selecting previously unselected package openssh-sftp-server.
Preparing to unpack …/6-openssh-sftp-server_10.2p1-1_aarch64.deb …
Unpacking openssh-sftp-server (10.2p1-1) … Selecting previously unselected package termux-auth.
Preparing to unpack …/7-termux-auth_1.5.0-1_aarch64.deb …
Unpacking termux-auth (1.5.0-1) … Selecting previously unselected package openssh.
Preparing to unpack …/8-openssh_10.2p1-1_aarch64.deb …
Unpacking openssh (10.2p1-1) … Setting up libedit (20240517-3.1-1) …
Setting up openssh-sftp-server (10.2p1-1) … Setting up ldns (1.8.4-1) …
Setting up git (2.53.0) … Setting up libresolv-wrapper (1.1.7-6) …
Setting up termux-auth (1.5.0-1) … Setting up libdb (18.1.40-5) …
Setting up krb5 (1.22.2) … Setting up openssh (10.2p1-1) …
Generating public/private rsa key pair. Your identification has been saved in /data/data/com.termux/files/usr/etc/ssh/ssh_host_rsa_key Your public key has been saved in /data/data/com.termux/files/usr/etc/ssh/ssh_host_rsa_key.pub The key fingerprint is: If you plan to use the 'ssh-agent'
it is recommended to run it as a service. Run 'pkg i termux-services'
to install the ('runit') service manager
You can enable the ssh-agent service using 'sv-enable ssh-agent'
You can also enable sshd to autostart using 'sv-enable sshd'
~/meshagora_prototype $ git clone https://github.com/JL-Kruger/meshagora.git
Cloning into 'meshagora'… remote: Enumerating objects: 17, done. remote: Counting objects: 100% (17/17), done.
remote: Compressing objects: 100% (11/11), done. remote: Total 17 (delta 5), reused 14 (delta 5), pack-reused 0 (from 0) Receiving objects: 100% (17/17), 49.96 KiB | 289.00 KiB/s, done. Resolving deltas: 100% (5/5), done.
~/meshagora_prototype $ python3 meshagora.py python3: can't open file '/data/data/com.termux/files/home/meshagora_prototype/meshagora.py': [Errno 2] No such file or directory
~/meshagora_prototype $ ls -a . .. meshagora
~/meshagora_prototype $ cd meshagora ~/meshagora_prototype/meshagora $ ls -a
. .gitignore README.md .. DEPLOY.md meshagora.py
.git LICENSE meshagora_spec_v2_3.md ~/meshagora_prototype/meshagora $ python3 meshagora.py ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡==
MESHAGORA v2.3 — SOCIAL SANDBOX Join URL : http://127.0.0.1:5000/join
Admin Post Key : 😡👻💀🤔 Trusted Post Key : 😀🌈
Default Post Key : 🌙😀 ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡==
Serving Flask app 'meshagora' Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. Running on all addresses (0.0.0.0)
Running on http://127.0.0.1:5000 * Running on http://127.0.0.1:5000
Press CTRL+C to quit ~/meshagora_prototype/meshagora

View File

@@ -0,0 +1,26 @@
#!/data/data/com.termux/files/usr/bin/bash
# Meshagora — Termux dependency installer
# Run once before first use: bash install_termux.sh
set -e
echo "========================================"
echo "Meshagora — Termux Setup"
echo "========================================"
echo ""
echo "[1/2] Installing system packages..."
pkg install -y python net-tools libjpeg-turbo libpng
echo ""
echo "[2/2] Installing Python packages..."
pip install flask "qrcode[pil]"
echo ""
echo "========================================"
echo "Setup complete."
echo ""
echo "Before starting the server:"
echo " 1. Enable Android mobile hotspot"
echo " 2. Run: python3 meshagora.py"
echo "========================================"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,457 @@
# Meshagora: Social Sandbox Specification (v2.3)
**Project**: Interactive local-network social platform for teaching digital resilience
**Framework**: 3SC — Safety (Caution), Sanity (Courage), Serenity (Competence)
**Values**: Trust (the value judgment), Respect (the mechanism), Freedom (the default state)
**Target Hardware**: Legacy Android devices (7.0+); iOS via iSH as stretch goal
**Deployment Context**: Classrooms and community spaces with unreliable or no internet
**Integration Point**: Section 2 of the 3SC Digital Safety Foundations workshop ("The Places It Can Take You")
---
## 1. What This Is
Meshagora is a temporary, local-only social platform that runs on an old phone serving as a WiFi hotspot and web server. Participants connect their devices to the hotspot and interact through a browser-based social feed — posting, taking notes, managing privacy, and exchanging trust tokens.
It is a micro-fediverse. One server, one community, one session. It teaches the structure of networked social interaction by making participants the administrators of their own social space.
The system is designed to degrade gracefully into chaos, then be rebuilt by the participants using tools the facilitator provides. The arc of each session moves from freedom through disorder to competence.
---
## 2. Pedagogical Design
### The Arc
**Phase 1 — The Open Square (Freedom)**
Everything is public. Profiles, notes, posts — all visible to all. This is the default state of "the internet before you configure it." The environment is functional but exposed.
**Phase 2 — The Noise (Loss of Sanity)**
The facilitator introduces disruption using the Admin Panel, ideally from a separate device (an old iPad or iPhone — see Section 7). This is announced in advance: *"At some point in this session, the system will be stressed. Your job is to recognize what's happening and build defenses."* This is a fire drill, not an ambush. Disruptions may include: creating sock puppet accounts that flood the feed, rotating the Default Post Key (silently breaking all default-tier users' posting ability), restricting a user (triggering a silent key rotation that affects everyone), showing participants their "private" data in the raw database, or demonstrating that anonymous posts aren't actually anonymous to the server admin.
**Phase 3 — The Build (Courage + Competence)**
All tools — privacy toggles, friend handshakes, friends-only view, report buttons — are available from the start. Participants simply have no motivation to use them until Phase 2 creates the need. Phase 3 is the moment participants start using what was always there. The facilitator may point out features participants haven't noticed ("Did you know you can make your notes private?") but does not unlock anything. The learning is in the transition from "I don't need that" to "I need that now."
**Phase 4 — The Reflection (Safety)**
Discussion: What happened? What did it feel like when the space was chaotic? When your notes were exposed? What changed when you had tools? How does this map to the platforms you actually use?
### 3SC Mapping
| Goal | Method | Sandbox Expression |
|------|--------|--------------------|
| **Safety** | Caution | Inspecting what's public before posting. Using the Profile Key to protect notes. Reporting posts that harm the space. Verifying links and content before trusting them. |
| **Sanity** | Courage | Setting boundaries in a noisy space. Choosing to filter rather than flee — or choosing to retreat to friends-only when the square becomes untenable. Confronting the discomfort of exposed data. Deciding whether to add your report to the count. |
| **Serenity** | Competence | Understanding *how* the privacy filters work. Knowing that "delete" means something specific in a database. Building the skill to configure your own environment. Recognizing that retreating to friends-only has consequences for the public square you left behind. |
### Connection to Broader Workshop
Meshagora gives participants direct experience of concepts introduced elsewhere in the 3SC workshop:
- **Section 1 (The Thing in Your Pocket)**: The server device demonstrates that phones are computers, not just screens. Participants see a phone *serving* content, not just consuming it.
- **Section 2 (The Places It Can Take You)**: Meshagora *is* the social platform. Its structure mirrors the Fediverse model from the ATProto/Fediverse primer — one server, community rules, local moderation. When discussing Mastodon or Bluesky later, participants have a visceral reference point.
- **Section 3 (The Creatures You Meet There)**: If a chatbot or automated poster is added in future extensions, participants have already seen how a "feed" works from the inside.
---
## 3. Technical Architecture
### Stack
| Component | Technology | Rationale |
|-----------|------------|-----------|
| Host environment | Termux (Android) | Reliable, well-maintained, real Linux userland |
| Language | Python 3 | Pre-installed in Termux, lowest barrier |
| Web framework | Flask | Single-file deployable, minimal dependencies |
| Data layer | SQLite (in-memory or file) | No external database server needed |
| Network | Android mobile hotspot | Creates LAN with no ISP dependency |
| Client | Any web browser | No app installation required for participants |
### Deployment
1. Facilitator installs Termux on the server device (Android 7.0+).
2. Facilitator runs `pkg install python` and `pip install flask qrcode[pil]`.
3. Facilitator enables mobile hotspot on the server device.
4. Facilitator runs the server script. On startup, the script:
- Creates a fresh SQLite database (no data persists from prior sessions).
- Detects the device's hotspot IP address.
- Generates a QR code image pointing to `http://[IP]:5000`.
- Serves the QR code at a facilitator-only URL (e.g., `/qr`) that can be shown or hidden. The facilitator displays this on the server device's screen during onboarding, then hides it once all participants have joined.
5. Participants join the hotspot WiFi and scan the QR code (or navigate to the displayed URL).
**Critical UX detail**: Nobody types IP addresses. The QR code is non-negotiable for classroom usability.
### Session Lifecycle
- The database is created when the server starts.
- The database is destroyed when the server stops.
- There is no persistent storage between sessions. This is a feature: every class starts clean.
- The facilitator controls the lifecycle by starting (`python app.py`) and stopping (`Ctrl+C`) the Flask process.
- Optional: The facilitator can trigger a mid-session database wipe to demonstrate data impermanence.
---
## 4. Data Model
### Users
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `session_label` | TEXT | System-assigned. Derived from id at creation (e.g., "Anon #1"). Used in public square and admin panel. |
| `username` | TEXT | Participant-chosen, case-sensitive, secret. Only visible to confirmed friends. |
| `profile_notes` | TEXT | Class notes, personal workspace |
| `notes_visible` | BOOLEAN | Default: TRUE (public). Toggled by Profile Key. |
| `post_tier` | TEXT | One of: `ADMIN`, `TRUSTED`, `DEFAULT`, `RESTRICTED`. Default: `DEFAULT`. |
| `created_at` | TIMESTAMP | Session-relative |
No email. No password hash. No PII. Identity exists only for the duration of the session.
Usernames are **pseudonymous by default**. In the public square, posts from non-friends display with an opaque session label (e.g., "Anon #7"). Posts from confirmed friends display the friend's username. This means the public square looks anonymous to newcomers and gradually becomes populated with known identities as friendships form. The only way to learn someone's username is through real-world interaction.
### Keys (Per-User)
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `user_id` | FK → Users | Owner |
| `key_type` | TEXT | One of: `PROFILE`, `FRIEND` |
| `emoji_code` | TEXT | Two-emoji string (e.g., "🍎🐶") |
Each user has exactly one key of each type. The FRIEND key is personal (not per-relationship).
### Post Keys (Server-Level)
Post keys are not per-user. They are server-wide secrets managed by the facilitator.
| Key | Set At | Changes? | Who Uses It |
|-----|--------|----------|-------------|
| **Admin Post Key** | Server startup (4-emoji) | Never | Facilitator. Also authenticates `/admin` panel and co-signs restricted user posts. |
| **Trusted Post Key** | Server startup (2-emoji) | Never | Participants promoted to TRUSTED tier by the facilitator. |
| **Default Post Key** | Server startup (2-emoji) | **Rotates** every time a user is restricted | All DEFAULT-tier participants. |
| **Restricted posting** | N/A | N/A | RESTRICTED users can only post when an admin co-signs with the Admin Post Key. |
**The rotation mechanic is the point.** When a user is restricted — whether by the facilitator or by community reports reaching 51% — the Default Post Key changes for *everyone*. No notification is sent. Participants discover the change when their next post attempt fails with a standard error message. They must come to the facilitator for the new key. The facilitator has a "distribute new Default Post Key" button in the Admin Panel but controls the pace and method of distribution.
### Handshakes
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `from_user_id` | FK → Users | The submitter |
| `to_user_id` | FK → Users | The target |
| `submitted_code` | TEXT | 4-emoji string: submitter's friend key + target's friend key |
| `created_at` | TIMESTAMP | When submitted |
A friendship is confirmed when the Handshakes table contains matching inverse entries: A→B with code F1F2 and B→A with code F2F1, where F1 is A's Friend Key and F2 is B's Friend Key.
### Posts
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `author_id` | FK → Users | Who posted |
| `content` | TEXT | Post body |
| `score` | INTEGER | Default: 0. Sum of weighted votes. |
| `created_at` | TIMESTAMP | For feed ordering |
The `score` field is the net result of all weighted votes, recalculated on each vote (write-time, not read-time). Most-positive score pins to top, most-negative pins to bottom.
### Votes
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `post_id` | FK → Posts | The post being voted on |
| `user_id` | FK → Users | The voter |
| `direction` | INTEGER | +1 (like) or -1 (dislike) |
One vote per user per post. The vote's *weight* is determined by the voter's tier at the time of voting:
| Voter Tier | Vote Weight |
|------------|-------------|
| ADMIN | +1 / -1 |
| TRUSTED | +2 / -2 |
| DEFAULT | +1 / -1 |
| RESTRICTED | 0 (vote is recorded but has no effect) |
Admin votes are deliberately weighted the same as default. The facilitator's power is structural (key management, user restriction), not social (inflated votes). Trusted users are the only tier with amplified voice — a privilege that's invisible unless the facilitator reveals it.
Vote weight is locked at time of voting. If a user is promoted from DEFAULT to TRUSTED, their past votes do not retroactively gain weight.
### Reports
| Field | Type | Notes |
|-------|------|-------|
| `id` | INTEGER, PK | Auto-increment |
| `post_id` | FK → Posts | The reported post |
| `reporter_id` | FK → Users | Who filed the report |
| `created_at` | TIMESTAMP | When reported |
One report per reporter per post. Reports have two escalating effects:
**Spoiler (per-post, immediate):** A single report on a post wraps it in a spoiler overlay — content hidden until clicked. The spoiler is a visual flag, not a removal. Anyone can click through to read the content. Additional reports on the same post don't change the spoiler — one is enough.
**Restriction (per-user, threshold):** When unique reporters across all of a user's posts reach 51% of total registered participants (excluding the reported user), the system **auto-restricts** that user. This triggers the Default Post Key rotation.
**"Active participants"** means total registered users in this session, minus the reported user. Flask doesn't maintain persistent connections, so there is no presence detection. The threshold is based on registration count, not who's currently looking at the screen.
**Design implications:**
- **The report button is on each post.** Tapping "report" on a post by Anon #7 reports that specific post and spoilers it. The system tracks unique reporters per user across all their posts for the restriction threshold.
- **Reported users are notified that they've been reported, but not by whom.** This creates accountability without enabling retaliation. The notification says something like "One of your posts has been reported" — no specifics about which post or who reported it.
- **The facilitator can restrict a user on a single report.** The community needs 51%; the admin needs only their judgment. This asymmetry between democratic and authoritarian moderation is the point.
- **Mob rule is not prevented.** Nothing stops a majority from restricting someone for a bad joke, an unpopular opinion, or for fun. Community moderation can be just and it can be tyrannical. The system doesn't distinguish.
- **Unrestriction is admin-only.** The community can restrict easily but cannot restore. This asymmetry between "easy to punish, hard to forgive" is real, worth experiencing, and worth discussing.
- **Friends can report friends.** Friendship does not grant immunity from accountability.
- **Restricted users can still report others.** Losing your voice doesn't strip your ability to flag behaviour. Whether that's fair is a discussion question.
### Trust Relationships (Linked Palindromic Pair)
Each user has a personal Friend Key — a 2-emoji combo chosen at profile creation.
**Handshake flow (user-facing):**
1. Select the target's username from a list of all registered usernames in the session.
2. Enter your own Friend Key (2 emoji).
3. Enter the target's Friend Key (2 emoji).
4. The system validates the username exists, concatenates (yours + theirs), and submits.
**Pseudonymity note:** The handshake screen exposes all registered usernames. Participants can browse the full list but cannot map names to real people in the room or to session labels — that still requires IRL exchange. If someone shouts "who's CoolPenguin42?" in class, the pseudonymity is broken socially, not technically. The system provides the cover; maintaining it requires discipline. This is an operational security lesson, not a bug.
The target must complete the same flow in reverse. The server validates:
- The first pair of the submitted code matches the submitter's stored Friend Key.
- The second pair matches the target's stored Friend Key.
- A matching inverse entry exists from the other side (target → submitter, with target's key first).
Example:
- A's Friend Key: 🍎🐶 — B's Friend Key: 🌙🔥
- A's submission (system-built): 🍎🐶🌙🔥 — B's submission: 🌙🔥🍎🐶
**Security analysis and pedagogical framing: see Section 5 (Friend Key).**
---
## 5. The Three Keys
### Profile Key 🔑
- **Purpose**: Ownership. Required to edit your profile notes and toggle note visibility.
- **Assigned**: On user creation. The participant chooses their own 2-emoji combo.
- **Lesson**: Your identity and your data require a key you control. If you lose it (forget it), you lose access to your own notes. Nobody else can recover it for you.
### Post Keys 📝 (Tiered, Server-Level)
Post keys are not personal — they are platform credentials managed by the facilitator. See Section 4 (Post Keys) for the key table and rotation trigger rules.
- **Admin Post Key** (4-emoji): Known only to the facilitator. Never changes. Used to post, co-sign restricted users' posts, perform admin actions, and authenticate the Admin Panel. The 4-emoji length (rather than 2) makes it harder to guess and provides a future teaching hook for pen-testing exercises.
- **Trusted Post Key**: Distributed by the facilitator to participants who earn TRUSTED status. Never changes. A stable credential that rewards demonstrated competence and good faith participation.
- **Default Post Key**: Distributed to all participants at the start of the session. **Rotates every time a user is restricted.** When it rotates, no one is notified. Participants discover their key is broken when they try to post and receive a standard error message. They must ask the facilitator for the new key. The facilitator has a "distribute new Default Post Key" button in the Admin Panel but chooses when and how to share it — verbally, on the board, whispered to individuals.
- **Restricted Posting**: A RESTRICTED user cannot post independently. They can only post if the facilitator co-signs with the Admin Post Key — meaning an admin must actively approve the post. This is supervised access.
**Lessons taught by this structure:**
- The difference between personal credentials (Profile Key, Friend Key) and platform access (Post Keys). You own one; someone else owns the other.
- Hierarchies of access exist on every platform. Some users are verified, some are shadowbanned, some are suspended. Now you've felt what each level is like.
- Moderation has externalities. Restricting one user disrupts every default user — and the disruption is silent. You only find out when you try to act.
- Stability as privilege. TRUSTED users are unaffected by key rotations. That stability is itself a form of power.
- Access recovery is a social process. The new key exists, but you have to ask for it. Who you have to ask, and how they choose to distribute it, shapes the community.
### Friend Key 🤝
- **Purpose**: Identity-bound trust. Your personal Friend Key is a 2-emoji combo set at profile creation. See Section 4 (Trust Relationships) for the handshake validation logic and data model.
- **Assigned**: Chosen by the participant at profile creation.
- **The Exchange Ritual**: To become friends, two participants must meet in person and share two things: their username (secret until this moment) and their Friend Key emoji pair. Each then uses the handshake screen to select the other's username from the registered list, enter both keys, and submit. The system concatenates and validates. The physical exchange is the entire point: trust is built face-to-face, then registered on the network.
- **Lesson**: Trust requires mutual action, shared knowledge, *and* verified identity. Knowing someone's secret isn't enough to impersonate them — the system checks who you are, not just what you know.
- **Security model**: The handshake is identity-bound. If C learns both A's and B's Friend Keys (eavesdropping, social engineering), C still cannot forge the A↔B connection. The system would build C's submission as [C's key + B's key] — which fails validation because C's key isn't A's key. The only way C can impersonate A is by changing C's own Friend Key to match A's — which requires compromising A's Profile Key. This creates a dependency chain: **Profile Key protects Friend Key protects trust relationships.** All keys are secrets. All are attackable. But knowledge alone isn't sufficient without identity.
- **Pseudonymity implication**: Friendship doesn't just unlock private notes — it unlocks *who someone is*. In the public square, non-friends are anonymous. Forming a friendship transforms an opaque session label into a known person. This directly teaches the difference between pseudonymity and identity, and why de-anonymization is a meaningful act of trust.
---
## 6. User Interface Constraints
The UI must work on damaged, old, and small-screen devices. These are hard design requirements, not suggestions.
**Aesthetic direction**: Post-soviet cyberpunk. Functional, raw, slightly brutalist. Monospace fonts, visible borders, no rounded-corner polish. The interface should feel like it was built in a garage from salvaged parts — because it was. This aesthetic is honest about what the system is (a prototype on old hardware) and appealing to younger participants who respond to the vibe of "we built this ourselves."
Functional constraints:
- **Large touch targets**: Minimum 48x48px for all interactive elements.
- **High contrast**: Dark text on light background. No subtle grays. No low-contrast placeholder text.
- **Minimal text**: Labels are short. Instructions use icons + words. The emoji-key input is the primary interaction, not typing.
- **No JavaScript frameworks**: The UI is server-rendered HTML with minimal inline JS. This keeps page weight low and avoids compatibility issues on old WebViews.
- **Works on cracked screens**: No interactions that require precision tapping in corners. Primary actions are centered.
- **Mobile-first**: The UI assumes a phone screen. Desktop is not a design target.
### Key Screens
1. **Join**: Choose a username (secret — explain this clearly) → choose Profile Key and Friend Key → receive the current Default Post Key. The system assigns a session label (e.g., "Anon #7") for public display.
2. **Square**: The public feed. Highest-scoring post pinned at top. Lowest-scoring post pinned at bottom. Both always visible (a toy recommendation algorithm — "this is what 'trending' means"). Remaining posts newest-first between the pinned extremes. Each post shows the author's session label or username (if the viewer is a confirmed friend). Like/dislike buttons on each post — weighted by voter tier, but the weighting is invisible to participants until the facilitator reveals it. Report button on each post; a single report spoilers the post (content hidden behind a click-to-reveal overlay). Post input requires the appropriate Post Key for the user's tier. **Friends-only toggle**: switches the feed to show only posts from confirmed friends, hiding the public square entirely.
3. **Profile**: Your notes editor. Toggle visibility (requires Profile Key). Friend Key management: select target's username from the registered list, enter your Friend Key, enter their Friend Key, submit. View confirmed friends and their usernames.
4. **Admin Panel** (`/admin`, authenticated by the 4-emoji Admin Post Key, entered once per browser session): Restrict or promote users by session label. View and distribute the current Default Post Key (the key is displayed on this screen so the facilitator can read it out, write it on a board, or whisper it). Trigger attack simulations (see Section 7). View raw database. Unrestrict users. Show/hide the join QR code.
**Platform View — "WHAT THE PLATFORM SEES"** (bottom of admin panel): A surveillance dashboard designed for mid-session or end-of-session projection. Three panels:
- *Deanonymization table*: every session label alongside real username, tier, post count, and net score — one screen collapses the pseudonymity the whole session was built on.
- *Social graph*: all handshake pairs (CONFIRMED / PENDING). The platform has always known the full network topology; participants knew only their own connections.
- *Behavioral correlations*: who voted for whom and who reported whom, cross-referenced against confirmed friendships. Shows that the platform can infer relationships from behavior alone — the metadata lesson. A `[FRIENDS]` badge appears where a correlation pair are confirmed friends.
---
## 7. Hardware Constraints: Honest Assessment
### Primary Target: Android via Termux
- **Minimum viable server device**: Any Android phone running 7.0+ with 1GB+ RAM.
- **Realistic capacity**: Flask on SQLite on a budget Android phone comfortably handles 15-20 concurrent browser clients on a local hotspot. This matches typical classroom size.
- **Battery**: A full session (2-3 hours) will drain the server device. Keep it plugged in.
- **Background kill**: Android aggressively kills background apps. The facilitator must ensure Termux has a persistent notification and battery optimization is disabled for Termux in device settings.
### Stretch Goal: iOS via iSH
- **What works**: iSH runs Alpine Linux on iOS 11+, which covers iPhone 7 and newer. Python 3 installs. Flask runs. It can serve pages on a local network.
- **What's slow**: iSH emulates x86 on ARM. Everything runs at a fraction of native speed. Server response times will be noticeably sluggish.
- **What doesn't work**: iOS aggressively suspends background apps. The iSH app must stay in the foreground with the screen on. This is fragile for a server role.
- **Recommendation**: Use iOS devices as *clients* (browsers), not servers. If an Android device is available, it should be the server.
### Legacy Apple Devices as Teaching Props and Attack Platforms
The iPad Air (1st gen) and iPhone 7 are not good Meshagora servers, but they have two valuable roles: demonstrating hardware repurposing and running adversarial simulations.
**Tinkering and Repurposing Demos:**
- **iSH install walkthrough**: Show participants that a Linux terminal can run on an old iPhone. The act of installing and typing `python3` on a phone is itself a teaching moment — the device is more than what Apple says it is.
- **Client participation**: Old iOS devices work fine as browsers connecting to Meshagora. They're participants in the network, not infrastructure.
- **Hardware teardown context**: "This phone has a GPS, accelerometer, gyroscope, barometer, camera, microphone, and a multi-core processor. It cost someone $800 in 2016. It now costs $40. What can you do with it?" This connects directly to Section 1 of the workshop.
- **Limitations as lessons**: The fact that iOS *restricts* background processes, server roles, and sideloading is itself a teaching point about platform control and walled gardens.
**The Attack Platform (Phase 2 Tool):**
Legacy iOS devices are ideal for running adversarial simulations because they're physically separate from the server, visible to the class, and just capable enough to fire HTTP requests.
- **MVP approach — Admin Panel as remote control**: The facilitator opens the Agora's `/admin` route in Safari on the iPad. From there they can trigger canned disruptions: create sock puppet accounts that flood the feed, rotate the Default Post Key, restrict users, and display the raw database. The iPad becomes a visible "attack console" — the class can see the facilitator tapping buttons on a separate device and watch the effects propagate to their screens in real time.
- **Post-MVP approach — Visible scripts**: A standalone Python script running in iSH on the iPad or iPhone 7 that hits Meshagora's API endpoints as fake users. The facilitator shows the script running on the device's screen: "This is what a bot looks like. These are the HTTP requests it sends. This is how a sock puppet account works." iSH is slow, but for sending POST requests to a local Flask server, it's more than adequate. Learners can read the code, see it execute, and correlate each request with what appears in the feed.
- **Why a separate device matters**: Running attacks from the same device as the server muddles the lesson. A physically distinct attack device teaches that threats come from outside your system, through the same network interface that legitimate users connect through. The iPad sitting on the teacher's desk running attack scripts is a tangible threat model.
### What Was Cut (and Why)
- **QPython**: Largely abandoned, unreliable. Not included.
- **BeeWare/Kivy packaging**: Building .apk files is out of scope for a classroom exercise.
- **Local LLM inference**: Not viable on target hardware. An iPhone 7 with 2GB RAM cannot usefully run even TinyLlama through iSH's x86 emulation layer.
- **Captive portal**: Requires root or custom firmware on most Android devices. The QR code approach solves the same discovery problem without this complexity.
---
## 8. Facilitator Guide Notes
### Before the Session
- Test the full setup on the actual server device at least once.
- Pre-install Termux, Python, Flask, and qrcode library.
- Set up the attack device (iPad/iPhone): bookmark the Admin Panel URL in Safari, or pre-install iSH with attack scripts if using the richer approach.
- Print 2-3 backup QR codes in case the on-screen code is hard to scan.
- Prepare a simple one-page handout: "Join the WiFi called [X], scan this code."
- Decide in advance which disruptions you'll run in Phase 2 and in what order.
### During the Session
- **Announce the threat model early**: "This system will be stressed during our session. Part of your job is to notice when that happens and respond."
- **Control pacing**: Don't rush from Phase 1 to Phase 2. Let participants settle into the open square before introducing disruption. 10-15 minutes of calm posting first.
- **Let pseudonymity develop naturally**: Don't explain the friend-reveals-username mechanic upfront. Let participants discover that their friends' posts suddenly have names while strangers remain anonymous. The surprise is the lesson.
- **Use the like/dislike pinning early**: Once a few posts accumulate, point out what's pinned at top and bottom. Ask: "Who decided what you see first? You did — collectively. Is the result good?" This is the gentlest possible introduction to algorithmic ranking.
- **Reveal vote weighting later**: After participants have internalized the ranking, reveal that trusted users' votes count double and restricted users' votes count for nothing. Ask: "Did you notice? Does this change how you feel about what's pinned at the top?" This is the covert-influence lesson — equal-looking buttons producing unequal outcomes.
- **Demonstrate the database**: At some point, show participants the raw SQLite file via the Admin Panel. Let them see their "private" notes and "secret" usernames in plain text. This is the single most impactful teaching moment in the session.
- **Restrict someone and let the ripple hit**: When you restrict a user, the Default Post Key rotates silently. Don't announce it. Wait for participants to discover their posts are failing and come to you. The queue of confused people at the facilitator's desk is the lesson. Discuss afterward: "Why did your key break? Who got restricted? Was it the admin or was it the community? Is this fair?"
- **Let the community report mechanic play out**: Don't intervene when spoilered posts start appearing. If the class auto-restricts someone unfairly, that's a better teaching moment than preventing it. Discuss afterward: "Was that just? What would due process look like? Who decides what's 'bad behaviour' when the majority rules?"
- **Key distribution is social, not automatic**: When the Default Post Key rotates, participants must come to you. You choose how to distribute: announce to the whole room, whisper to individuals, write it on the board, or make them earn it. Each distribution method teaches something different about platform access and information asymmetry.
- **Watch for the friends-only retreat**: When participants start switching to friends-only view, the public square will thin out. Voting activity drops, pinned posts stagnate, the feed feels dead. Point this out: "What happened to the public square? Where did everyone go? What does it mean when the engaged users leave?" This is platform death spiral in miniature.
- **Resist fixing things for participants**: When the noise hits, point participants toward features they haven't used yet — but don't implement anything for them. The learning is in the doing.
### After the Session
- Stop the server. The database is gone. Emphasize this: "Everything we built today is gone. What remains is what you learned."
- In multi-session courses: the clean slate is intentional. Learners carry forward the social norms, agreements, and competence they built — but the technical environment resets. They rebuild faster each time. The knowledge is in the humans, not the machine.
- Connect to the broader workshop: "The platforms you use every day have this same structure — servers, databases, keys, feeds, ranking algorithms, moderation tiers. The difference is you can't see inside them. Now you know what's in there."
---
## 9. Minimum Viable Product
The MVP is the smallest buildable version that delivers the pedagogical arc. Everything below this line must work before any extensions are considered.
### MVP Features
1. User creation with secret username (case-sensitive string) and system-assigned sequential session label.
2. Pseudonymous public square: posts show session labels to non-friends, usernames to confirmed friends.
3. Tiered Post Keys: Admin (4-emoji), Trusted (2-emoji), Default (2-emoji, rotating), and Restricted posting (admin co-sign required). Admin Post Key also authenticates the Admin Panel.
4. Like/dislike on posts with tier-weighted scoring (Trusted +2, Default/Admin +1, Restricted 0). Highest-scoring pinned to top, lowest to bottom. Weight locked at time of vote.
5. Per-post report button. Single report spoilers the post (click-to-reveal). 51% unique reporters across a user's posts auto-restricts them. Facilitator can restrict on a single report. Reported users are notified but not told who reported them. Unrestriction is admin-only.
6. Friends-only feed toggle: hides the public square, shows only posts from confirmed friends.
7. Profile page with notes editor (requires Profile Key to edit).
8. Notes visibility toggle (public/private, requires Profile Key).
9. Friend Key handshake: select target from registered username list, enter your key, enter their key, system concatenates and validates the linked palindromic pair. Friendship reveals username on the public feed.
10. Private note viewing for confirmed friends.
11. Admin Panel (authenticated by 4-emoji Admin Post Key): user management, distribute new Default Post Key, attack triggers, unrestriction, raw database view, QR show/hide.
12. QR code generation on server startup with show/hide control.
13. Session reset on server restart. Learners carry forward social norms and knowledge; the database does not persist.
### MVP Delivers
- The full Phase 14 pedagogical arc.
- Tangible experience of pseudonymity, trust hierarchies, platform moderation externalities, algorithmic content ranking, community justice (and its failures), and the retreat-to-trust-network dynamic.
- Both top-down (admin) and bottom-up (community report) moderation in the same system, with visible tradeoffs.
- A working micro-fediverse that runs on a single old phone, attacked from an old iPad.
---
## 10. Extensions
### Implemented (Beyond MVP)
**Platform View — Admin Surveillance Dashboard** *(implemented)*
Described in Section 6. No schema changes — pure SQL joins on existing tables, rendered as three panels at the bottom of the Admin Panel. See Section 6.4 for full description.
### Future Extensions (Post-MVP)
These are documented for future development. None are required for the core experience.
### Chaos Bot (Advanced)
The MVP admin panel supports basic sock puppet and flood attacks. This extension adds autonomous adversarial agents: bots that adapt their behaviour based on the feed state (e.g., targeting the most-liked post with dislikes, mimicking a specific user's posting style, or gradually escalating provocative content). Teaches pattern recognition and the difference between scripted spam and adaptive manipulation.
### Privacy Filter Workshop
Instead of a simple visibility toggle, participants write or configure their own filter rules (e.g., "show my notes only to users with reputation > X"). Teaches algorithmic moderation.
### Reputation System
A community-driven trust score based on peer endorsements. Teaches how reputation systems work (and how they can be gamed).
### Auto-Trust by Social Graph
A facilitator-toggled mechanic: when enabled, any user with confirmed friendships exceeding half the active participants is automatically promoted to TRUSTED tier. This teaches that platforms often grant authority based on network metrics (follower count, engagement, connections) rather than demonstrated competence. Best introduced mid-session after participants have experienced manual trust promotion, so the class can contrast the two models and discuss what changes when popularity becomes power. Creates a perverse incentive to collect friendships as tokens rather than trust relationships — surfacing that incentive *is* the lesson.
### Offline Knowledge Library
Integrate Kiwix (offline Wikipedia/Project Gutenberg) as a second service on the server device. The "survival information" component — demonstrating that a phone can be a library, not just a communication device.
### Cross-Device Federation
Run two Meshagora instances on two devices on the same network. Posts federate between them. Directly demonstrates the Fediverse model with physical hardware participants can see and touch.
### Agent Integration
Use an AI coding agent to generate the Flask server code from this spec, with participants auditing the output. Teaches code review and AI literacy simultaneously.
---
## 11. Specification Status
This is a **v2.3 hybrid human-model design document**. It has been:
- Generated through conversation with Gemini (v1.0 and v1.1).
- Reviewed and critiqued by Opus for technical accuracy, framework alignment, and scope control.
- Refined with corrected framework language (Safety/Sanity/Serenity), linked palindromic friend-key mechanics, honest hardware constraints, iOS tinkering use cases, and integration with existing 3SC workshop materials.
- Extended with tiered post keys, like/dislike ranking, pseudonymous identity, community reports, friends-only feed, and legacy iOS as attack platform.
- Final review pass: resolved agent-facing ambiguities (admin auth, handshake flow, key distribution, vote weight locking, report notification, active participant threshold, Phase 3 feature-gating), reduced duplication between Sections 4 and 5, documented pseudonymity implications of username list.
It is ready for:
- Implementation by a coding agent (targeting a single `app.py` file).
- Human audit of the generated code.
- Pilot testing on actual legacy hardware.
It is **not** a finished product. It is a buildable specification for a prototype.
---
*Design note: This specification is intended to be stored on the server device itself. The documentation is the first artifact in the lab.*

View File

@@ -0,0 +1,19 @@
# TNT: Tools N Toys - JL notes
---
in here are *-tools.md, where * is the category name. the files are lists of links. there are also folders, and other files which are themselves, tools or toys.
---
render links as cards with some useful information and a visit button.
render html in a full width iframe
if possible to load an application into the browser, go ahead, elsewise make a card with info and a download button.
---
tools are downloadable. links are not.