feat: add documentation hub with architecture, design, api, bots, and contribute pages
perf: increase max workers in channel message service from 1 to 10 docs: update navigation links in about and index templates
This commit is contained in:
parent
5b241038f1
commit
7f2a5615b3
@ -27,6 +27,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Version 1.28.0 - 2026-01-17
|
||||||
|
|
||||||
|
Adds a documentation hub with pages on architecture, design, API, bots, and contributions. Increases the maximum workers in the channel message service to 10 for improved performance and updates navigation links in the about and index pages.
|
||||||
|
|
||||||
|
**Changes:** 11 files, 2755 lines
|
||||||
|
**Languages:** HTML (2719 lines), Python (36 lines)
|
||||||
|
|
||||||
## Version 1.27.0 - 2026-01-16
|
## Version 1.27.0 - 2026-01-16
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Snek"
|
name = "Snek"
|
||||||
version = "1.27.0"
|
version = "1.28.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
#license = { file = "LICENSE", content-type="text/markdown" }
|
#license = { file = "LICENSE", content-type="text/markdown" }
|
||||||
description = "Snek Chat Application by Molodetz"
|
description = "Snek Chat Application by Molodetz"
|
||||||
|
|||||||
@ -44,6 +44,7 @@ from snek.view.about import AboutHTMLView, AboutMDView
|
|||||||
from snek.view.avatar import AvatarView
|
from snek.view.avatar import AvatarView
|
||||||
from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, ChannelView
|
from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, ChannelView
|
||||||
from snek.view.docs import DocsHTMLView, DocsMDView
|
from snek.view.docs import DocsHTMLView, DocsMDView
|
||||||
|
from snek.view.site import ArchitectureView, DesignView, ApiView, BotsView, ContributeView
|
||||||
from snek.view.drive import DriveApiView, DriveView
|
from snek.view.drive import DriveApiView, DriveView
|
||||||
from snek.view.channel import ChannelDriveApiView
|
from snek.view.channel import ChannelDriveApiView
|
||||||
from snek.view.container import ContainerView
|
from snek.view.container import ContainerView
|
||||||
@ -299,6 +300,11 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/logout.html", LogoutView)
|
self.router.add_view("/logout.html", LogoutView)
|
||||||
self.router.add_view("/docs.html", DocsHTMLView)
|
self.router.add_view("/docs.html", DocsHTMLView)
|
||||||
self.router.add_view("/docs.md", DocsMDView)
|
self.router.add_view("/docs.md", DocsMDView)
|
||||||
|
self.router.add_view("/architecture.html", ArchitectureView)
|
||||||
|
self.router.add_view("/design.html", DesignView)
|
||||||
|
self.router.add_view("/api.html", ApiView)
|
||||||
|
self.router.add_view("/bots.html", BotsView)
|
||||||
|
self.router.add_view("/contribute.html", ContributeView)
|
||||||
self.router.add_view("/status.json", StatusView)
|
self.router.add_view("/status.json", StatusView)
|
||||||
self.router.add_view("/settings/index.html", SettingsIndexView)
|
self.router.add_view("/settings/index.html", SettingsIndexView)
|
||||||
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
self.router.add_view("/settings/profile.html", SettingsProfileView)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class ChannelMessageService(BaseService):
|
|||||||
self._executor_pools = {}
|
self._executor_pools = {}
|
||||||
global jinja2_env
|
global jinja2_env
|
||||||
jinja2_env = self.app.jinja2_env
|
jinja2_env = self.app.jinja2_env
|
||||||
self._max_workers = 1
|
self._max_workers = 10
|
||||||
|
|
||||||
def get_or_create_executor(self, uid):
|
def get_or_create_executor(self, uid):
|
||||||
if not uid in self._executor_pools:
|
if not uid in self._executor_pools:
|
||||||
|
|||||||
@ -145,8 +145,14 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="home-link" href="/">
|
<a class="home-link" href="/">
|
||||||
<img src="/image/snek_logo_256x256.png" alt="Snek Home Logo" />
|
<img src="/image/snek_logo_256x256.png" alt="Snek Home Logo" />
|
||||||
Snek Community
|
Snek
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html" class="active">About</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<header class="container about-hero">
|
<header class="container about-hero">
|
||||||
|
|||||||
@ -1,10 +1,290 @@
|
|||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
{% block main %}
|
<head>
|
||||||
<div class="dialog">
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<fancy-button size="auto" text="Back" url="/back"></fancy-button>
|
<title>Documentation - Snek Platform</title>
|
||||||
<html-frame url="/docs.md"></html-frame>
|
<meta name="description" content="Snek platform documentation hub. Learn about architecture, API, bot development, and how to contribute." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.doc-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
.doc-card {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.doc-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.doc-card h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
.doc-card p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.doc-card .arrow {
|
||||||
|
color: #0fa;
|
||||||
|
float: right;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.section pre {
|
||||||
|
background: #222;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7ef;
|
||||||
|
}
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.doc-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html" class="active">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek Logo" />
|
||||||
|
<h1>Documentation</h1>
|
||||||
|
<p>Learn how to use, extend, and contribute to the Snek platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<nav class="doc-grid">
|
||||||
|
<a href="/architecture.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>Architecture</h2>
|
||||||
|
<p>Understand the technical architecture: backend services, frontend modules, database design, and real-time communication.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/design.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>Design Decisions</h2>
|
||||||
|
<p>Learn why specific technologies were chosen and the philosophy behind the platform's design.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/api.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>API Reference</h2>
|
||||||
|
<p>Complete WebSocket RPC API documentation with all available methods for channels, messages, and more.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/bots.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>Bot Development</h2>
|
||||||
|
<p>Build automated bots with complete Python and JavaScript examples. Integrate with AI services.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/contribute.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>Contribute</h2>
|
||||||
|
<p>Set up development environment, understand project structure, and learn how to add features.</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/about.html" class="doc-card">
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>Learn about the platform's mission, principles, and why it exists.</p>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Quick Start</h2>
|
||||||
|
<p>Get Snek running locally in under a minute:</p>
|
||||||
|
<pre>pip install git+https://retoor.molodetz.nl/retoor/snek.git
|
||||||
|
snek serve</pre>
|
||||||
|
<p>The server starts at <code>http://localhost:8080</code>. Register a user and start chatting.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Key Features</h2>
|
||||||
|
<p>Snek is a privacy-focused platform for developers:</p>
|
||||||
|
<ul style="list-style: disc inside; margin-bottom: 1rem;">
|
||||||
|
<li>Real-time chat with WebSocket communication</li>
|
||||||
|
<li>File sharing via WebDAV and SFTP</li>
|
||||||
|
<li>Git repository hosting</li>
|
||||||
|
<li>In-browser Ubuntu terminal</li>
|
||||||
|
<li>AI integration for chat assistance</li>
|
||||||
|
<li>Bot development support</li>
|
||||||
|
<li>No email required, no activity logging</li>
|
||||||
|
<li>Self-hosting with single command deployment</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Connection Details</h2>
|
||||||
|
<p>Access the platform via multiple protocols:</p>
|
||||||
|
<ul style="list-style: none; margin-bottom: 1rem;">
|
||||||
|
<li><strong>WebDAV:</strong> <code>davs://molodetz.online/webdav</code></li>
|
||||||
|
<li><strong>SFTP:</strong> <code>sftp://molodetz.online:2242</code></li>
|
||||||
|
<li><strong>Git:</strong> <code>git@molodetz.online:username/repo.git</code></li>
|
||||||
|
<li><strong>WebSocket:</strong> <code>wss://snek.community/rpc.ws</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>All protocols use your Snek credentials.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@ -168,6 +168,7 @@
|
|||||||
<p>Professional Platform for Developers, Testers & AI Professionals</p>
|
<p>Professional Platform for Developers, Testers & AI Professionals</p>
|
||||||
<a href="/login.html" class="btn">Login</a>
|
<a href="/login.html" class="btn">Login</a>
|
||||||
<a href="/register.html" class="btn btn-primary">Register</a>
|
<a href="/register.html" class="btn btn-primary">Register</a>
|
||||||
|
<a href="/docs.html" class="about-link">Docs</a>
|
||||||
<a href="/about.html" class="about-link">About</a>
|
<a href="/about.html" class="about-link">About</a>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
|||||||
630
src/snek/templates/site/api.html
Normal file
630
src/snek/templates/site/api.html
Normal file
@ -0,0 +1,630 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>API Reference - Snek Platform Documentation</title>
|
||||||
|
<meta name="description" content="Complete API reference for the Snek platform WebSocket RPC interface. Documentation for all available methods including authentication, channels, messages, and more." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.section pre {
|
||||||
|
background: #222;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #7ef;
|
||||||
|
}
|
||||||
|
.method {
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
}
|
||||||
|
.method:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.method-name {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #0fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.method-sig {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.method-desc {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.method-params {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.method-params strong {
|
||||||
|
color: #f05a28;
|
||||||
|
}
|
||||||
|
.method-returns {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.method-returns strong {
|
||||||
|
color: #7ef;
|
||||||
|
}
|
||||||
|
.auth-required {
|
||||||
|
display: inline-block;
|
||||||
|
background: #f05a28;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.toc {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.toc h3 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.toc ul {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.toc a {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.toc ul {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html" class="active">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<h1>API Reference</h1>
|
||||||
|
<p>WebSocket RPC interface documentation</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<nav class="toc">
|
||||||
|
<h3>Categories</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#protocol">Protocol</a></li>
|
||||||
|
<li><a href="#authentication">Authentication</a></li>
|
||||||
|
<li><a href="#user">User</a></li>
|
||||||
|
<li><a href="#channels">Channels</a></li>
|
||||||
|
<li><a href="#messages">Messages</a></li>
|
||||||
|
<li><a href="#presence">Presence</a></li>
|
||||||
|
<li><a href="#container">Container</a></li>
|
||||||
|
<li><a href="#database">Database</a></li>
|
||||||
|
<li><a href="#utility">Utility</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="section" id="protocol">
|
||||||
|
<h2>Protocol</h2>
|
||||||
|
<p>Snek uses a JSON-RPC style protocol over WebSocket. Connect to <code>/rpc.ws</code> to establish a connection.</p>
|
||||||
|
|
||||||
|
<h3 style="color:#0fa;margin:1rem 0 0.5rem 0;">Request Format</h3>
|
||||||
|
<pre>{
|
||||||
|
"callId": "unique-id",
|
||||||
|
"method": "method_name",
|
||||||
|
"args": [arg1, arg2, ...]
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3 style="color:#0fa;margin:1rem 0 0.5rem 0;">Response Format</h3>
|
||||||
|
<pre>{
|
||||||
|
"callId": "unique-id",
|
||||||
|
"success": true,
|
||||||
|
"data": { ... }
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3 style="color:#0fa;margin:1rem 0 0.5rem 0;">Server Events</h3>
|
||||||
|
<p>The server pushes events without a callId:</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "...",
|
||||||
|
"event": "new_message",
|
||||||
|
"data": { ... }
|
||||||
|
}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="authentication">
|
||||||
|
<h2>Authentication</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">login</span><span class="method-sig">(username, password)</span>
|
||||||
|
<p class="method-desc">Authenticate and establish a session. On success, the WebSocket connection is associated with the user and subscribed to all their channels.</p>
|
||||||
|
<p class="method-params"><strong>username</strong> (string) - The username</p>
|
||||||
|
<p class="method-params"><strong>password</strong> (string) - The password</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> User record (without password field) on success, error on failure</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">ping</span><span class="method-sig">()</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Keep the connection alive and update the user's last activity timestamp.</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{"pong": []}</code></p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="user">
|
||||||
|
<h2>User</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_user</span><span class="method-sig">(user_uid=None)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get user information. If no user_uid is provided, returns the current user's data.</p>
|
||||||
|
<p class="method-params"><strong>user_uid</strong> (string, optional) - Target user's UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> User record (email hidden for other users)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">search_user</span><span class="method-sig">(query)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Search for users by username.</p>
|
||||||
|
<p class="method-params"><strong>query</strong> (string) - Search query</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of matching usernames</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="channels">
|
||||||
|
<h2>Channels</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_channels</span><span class="method-sig">()</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get all channels the current user is a member of.</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of channel objects with name, uid, tag, new_count, is_moderator, is_read_only, color</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">create_channel</span><span class="method-sig">(name, description=None, is_private=False)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Create a new channel. The creating user becomes the moderator.</p>
|
||||||
|
<p class="method-params"><strong>name</strong> (string) - Channel name</p>
|
||||||
|
<p class="method-params"><strong>description</strong> (string, optional) - Channel description</p>
|
||||||
|
<p class="method-params"><strong>is_private</strong> (boolean, optional) - Whether the channel is private</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true, uid: "...", name: "..."}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">update_channel</span><span class="method-sig">(channel_uid, name, description=None, is_private=False)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Update channel settings. Only the channel creator or admin can update.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>name</strong> (string) - New channel name</p>
|
||||||
|
<p class="method-params"><strong>description</strong> (string, optional) - New description</p>
|
||||||
|
<p class="method-params"><strong>is_private</strong> (boolean, optional) - Privacy setting</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true, name: "..."}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">delete_channel</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Delete a channel (soft delete). Only the channel creator or admin can delete. Public channel cannot be deleted.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">invite_user</span><span class="method-sig">(channel_uid, username)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Invite a user to a channel. Requires membership in the channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>username</strong> (string) - Username to invite</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true, username: "..."}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">leave_channel</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Leave a channel. Cannot leave the public channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">clear_channel</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Clear all messages from a channel. Admin only.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="messages">
|
||||||
|
<h2>Messages</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">send_message</span><span class="method-sig">(channel_uid, message, is_final=True)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Send a message to a channel. If is_final is false, the message can be updated (for typing indicators).</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Target channel UID</p>
|
||||||
|
<p class="method-params"><strong>message</strong> (string) - Message content</p>
|
||||||
|
<p class="method-params"><strong>is_final</strong> (boolean, optional) - Whether the message is final (default true)</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Message UID on success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_messages</span><span class="method-sig">(channel_uid, offset=0, timestamp=None)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get messages from a channel with pagination.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>offset</strong> (integer, optional) - Pagination offset</p>
|
||||||
|
<p class="method-params"><strong>timestamp</strong> (string, optional) - Filter by timestamp</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of message objects</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">update_message_text</span><span class="method-sig">(message_uid, text)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Update a non-final message's text. Only the message author can update. Message must be less than 8 seconds old.</p>
|
||||||
|
<p class="method-params"><strong>message_uid</strong> (string) - Message UID</p>
|
||||||
|
<p class="method-params"><strong>text</strong> (string) - New text content</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> <code>{success: true}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">finalize_message</span><span class="method-sig">(message_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Mark a message as final, preventing further updates.</p>
|
||||||
|
<p class="method-params"><strong>message_uid</strong> (string) - Message UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">mark_as_read</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Mark all messages in a channel as read for the current user.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_first_unread_message_uid</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get the UID of the first unread message in a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Message UID or null</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="presence">
|
||||||
|
<h2>Presence</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">set_typing</span><span class="method-sig">(channel_uid, color=None)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Broadcast a typing indicator to channel members.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>color</strong> (string, optional) - Indicator color (defaults to user's color)</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_online_users</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get users currently online in a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of user objects sorted by nick</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_recent_users</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get users with recent activity in a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array with uid, username, nick, last_ping</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_users</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get all members of a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array with uid, username, nick, last_ping</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="container">
|
||||||
|
<h2>Container</h2>
|
||||||
|
<p>Container methods provide access to channel-specific Docker containers (Ubuntu terminals).</p>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_container</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get container information for a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Container object with name, cpus, memory, image, status</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">start_container</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Start the container for a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">stop_container</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Stop the container for a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">get_container_status</span><span class="method-sig">(channel_uid)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get the current status of a channel's container.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Status string</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">write_container</span><span class="method-sig">(channel_uid, content, timeout=3)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Write to container stdin and capture output.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>content</strong> (string) - Content to write to stdin</p>
|
||||||
|
<p class="method-params"><strong>timeout</strong> (integer, optional) - Wait timeout in seconds (max 30)</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Output string</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="database">
|
||||||
|
<h2>Database</h2>
|
||||||
|
<p>Database methods provide direct access to user-scoped database operations.</p>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_insert</span><span class="method-sig">(table_name, record)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Insert a record into a user-scoped table.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Record to insert</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Inserted record</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_update</span><span class="method-sig">(table_name, record)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Update a record in a user-scoped table.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Record with updates</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Updated record</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_delete</span><span class="method-sig">(table_name, record)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Delete a record from a user-scoped table.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Record identifier</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_get</span><span class="method-sig">(table_name, record)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Get a single record from a user-scoped table.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Query criteria</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Record or null</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_find</span><span class="method-sig">(table_name, record)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Find records in a user-scoped table.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Query criteria</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of records</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_upsert</span><span class="method-sig">(table_name, record, keys)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Insert or update a record based on key fields.</p>
|
||||||
|
<p class="method-params"><strong>table_name</strong> (string) - Table name</p>
|
||||||
|
<p class="method-params"><strong>record</strong> (object) - Record data</p>
|
||||||
|
<p class="method-params"><strong>keys</strong> (array) - Key fields for matching</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Upserted record</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">db_query</span><span class="method-sig">(sql, args)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Execute a raw SQL query on user-scoped data.</p>
|
||||||
|
<p class="method-params"><strong>sql</strong> (string) - SQL query</p>
|
||||||
|
<p class="method-params"><strong>args</strong> (array) - Query parameters</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Query results</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">query</span><span class="method-sig">(sql)</span>
|
||||||
|
<span class="auth-required">Auth Required</span>
|
||||||
|
<p class="method-desc">Execute a read-only SQL query. Forbidden keywords (DROP, ALTER, etc.) are blocked.</p>
|
||||||
|
<p class="method-params"><strong>sql</strong> (string) - SQL SELECT query</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Array of records (sensitive fields filtered)</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="utility">
|
||||||
|
<h2>Utility</h2>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">echo</span><span class="method-sig">(*args)</span>
|
||||||
|
<p class="method-desc">Echo back the provided arguments. Useful for testing.</p>
|
||||||
|
<p class="method-params"><strong>args</strong> - Any arguments</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> The same arguments</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">echo_raw</span><span class="method-sig">(obj)</span>
|
||||||
|
<p class="method-desc">Send a raw JSON object through the WebSocket. No response is sent back.</p>
|
||||||
|
<p class="method-params"><strong>obj</strong> (object) - Object to send</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> No response</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method">
|
||||||
|
<span class="method-name">stars_render</span><span class="method-sig">(channel_uid, message)</span>
|
||||||
|
<p class="method-desc">Broadcast a stars render event to all online users in a channel.</p>
|
||||||
|
<p class="method-params"><strong>channel_uid</strong> (string) - Channel UID</p>
|
||||||
|
<p class="method-params"><strong>message</strong> (string) - Message content</p>
|
||||||
|
<p class="method-returns"><strong>Returns:</strong> Boolean success</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
367
src/snek/templates/site/architecture.html
Normal file
367
src/snek/templates/site/architecture.html
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Architecture - Snek Platform Documentation</title>
|
||||||
|
<meta name="description" content="Technical architecture documentation for the Snek platform. Learn about the layered architecture, backend services, frontend modules, and real-time communication systems." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section h3 {
|
||||||
|
color: #0fa;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section p, .section ul {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section ul {
|
||||||
|
list-style: disc inside;
|
||||||
|
}
|
||||||
|
.section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.section pre {
|
||||||
|
background: #222;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #7ef;
|
||||||
|
}
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
.arch-diagram {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #ccc;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.arch-diagram .layer {
|
||||||
|
border: 1px dashed #444;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.arch-diagram .layer-name {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html" class="active">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<h1>Architecture</h1>
|
||||||
|
<p>Technical architecture and system design of the Snek platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="section">
|
||||||
|
<h2>System Overview</h2>
|
||||||
|
<p>Snek follows a layered architecture pattern with clear separation of concerns. The system is built on Python's aiohttp framework for asynchronous HTTP and WebSocket handling.</p>
|
||||||
|
<div class="arch-diagram">
|
||||||
|
<div class="layer">
|
||||||
|
<div class="layer-name">Frontend Layer</div>
|
||||||
|
Vanilla JavaScript ES6 Modules | Custom Elements | WebSocket Client
|
||||||
|
</div>
|
||||||
|
<div class="layer">
|
||||||
|
<div class="layer-name">View Layer</div>
|
||||||
|
HTTP Handlers | WebSocket RPC | Template Rendering | Form Processing
|
||||||
|
</div>
|
||||||
|
<div class="layer">
|
||||||
|
<div class="layer-name">Service Layer</div>
|
||||||
|
Business Logic | Caching | Event Broadcasting | Authentication
|
||||||
|
</div>
|
||||||
|
<div class="layer">
|
||||||
|
<div class="layer-name">Mapper Layer</div>
|
||||||
|
Data Access | CRUD Operations | Soft Delete | Query Building
|
||||||
|
</div>
|
||||||
|
<div class="layer">
|
||||||
|
<div class="layer-name">Database Layer</div>
|
||||||
|
SQLite | Dataset ORM | Schema Management
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Backend Architecture</h2>
|
||||||
|
|
||||||
|
<h3>aiohttp Framework</h3>
|
||||||
|
<p>The backend is built on aiohttp, a high-performance asynchronous HTTP client/server framework. Key features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fully asynchronous request handling with async/await</li>
|
||||||
|
<li>Native WebSocket support for real-time communication</li>
|
||||||
|
<li>Session management via aiohttp-session</li>
|
||||||
|
<li>Static file serving and template rendering</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Service Layer Pattern</h3>
|
||||||
|
<p>Services encapsulate business logic and are accessed via a singleton registry:</p>
|
||||||
|
<pre>user = await app.services.user.get(uid=user_uid)
|
||||||
|
channel = await app.services.channel.create(label="#general")</pre>
|
||||||
|
<p>Available services include: user, channel, channel_member, channel_message, chat, socket, container, db, and more.</p>
|
||||||
|
|
||||||
|
<h3>Mapper Pattern</h3>
|
||||||
|
<p>Mappers provide data access abstraction with consistent CRUD operations:</p>
|
||||||
|
<pre>await mapper.get(uid=uid)
|
||||||
|
await mapper.find(field=value)
|
||||||
|
await mapper.save(model)
|
||||||
|
async for record in mapper.query(sql, params): ...</pre>
|
||||||
|
<p>All mappers support soft delete via the <code>deleted_at</code> field.</p>
|
||||||
|
|
||||||
|
<h3>Caching Strategy</h3>
|
||||||
|
<p>The service layer implements UID-based caching for frequently accessed data. Cache invalidation occurs on model updates.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Frontend Architecture</h2>
|
||||||
|
|
||||||
|
<h3>Vanilla JavaScript Philosophy</h3>
|
||||||
|
<p>The frontend uses pure ES6 JavaScript modules without any frameworks. This ensures:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Minimal bundle size and fast load times</li>
|
||||||
|
<li>No framework upgrade cycles or breaking changes</li>
|
||||||
|
<li>Direct DOM manipulation for maximum performance</li>
|
||||||
|
<li>Easy debugging without transpilation</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Module Structure</h3>
|
||||||
|
<pre>src/snek/static/
|
||||||
|
app.js # Main Application class (singleton)
|
||||||
|
socket.js # WebSocket RPC client
|
||||||
|
chat-window.js # Chat interface component
|
||||||
|
message-list.js # Message rendering
|
||||||
|
chat-input.js # Input handling
|
||||||
|
...</pre>
|
||||||
|
|
||||||
|
<h3>Custom Elements</h3>
|
||||||
|
<p>UI components are implemented as Custom Elements:</p>
|
||||||
|
<pre><chat-window></chat-window>
|
||||||
|
<message-list></message-list>
|
||||||
|
<channel-menu></channel-menu>
|
||||||
|
<nav-menu></nav-menu></pre>
|
||||||
|
<p>Each component has its own CSS file (no Shadow DOM per project guidelines).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Database Design</h2>
|
||||||
|
|
||||||
|
<h3>Dataset ORM</h3>
|
||||||
|
<p>Snek uses Dataset, a simple database abstraction layer built on SQLAlchemy. It provides:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Automatic table creation and schema inference</li>
|
||||||
|
<li>Dictionary-based record access</li>
|
||||||
|
<li>Transaction support</li>
|
||||||
|
<li>Raw SQL query execution when needed</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Core Tables</h3>
|
||||||
|
<pre>user # User accounts
|
||||||
|
channel # Chat channels
|
||||||
|
channel_member # Channel membership
|
||||||
|
channel_message# Chat messages
|
||||||
|
session # User sessions
|
||||||
|
...</pre>
|
||||||
|
|
||||||
|
<h3>Soft Delete Pattern</h3>
|
||||||
|
<p>Records are never physically deleted. Instead, the <code>deleted_at</code> timestamp is set. All queries filter by <code>deleted_at IS NULL</code> by default.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Real-time Communication</h2>
|
||||||
|
|
||||||
|
<h3>WebSocket RPC System</h3>
|
||||||
|
<p>Real-time features use a JSON-RPC style protocol over WebSocket:</p>
|
||||||
|
<pre>{
|
||||||
|
"callId": "1",
|
||||||
|
"method": "send_message",
|
||||||
|
"args": ["channel_uid", "Hello world", true]
|
||||||
|
}</pre>
|
||||||
|
<p>The server responds with:</p>
|
||||||
|
<pre>{
|
||||||
|
"callId": "1",
|
||||||
|
"success": true,
|
||||||
|
"data": "message_uid"
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>Event Broadcasting</h3>
|
||||||
|
<p>Server-initiated events are broadcast to channel subscribers:</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "...",
|
||||||
|
"event": "new_message",
|
||||||
|
"data": { ... }
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>Channel Subscription Model</h3>
|
||||||
|
<p>Users automatically subscribe to all their channels on WebSocket connection. The socket service manages subscriptions and delivers messages to the appropriate clients.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Directory Structure</h2>
|
||||||
|
<pre>src/snek/
|
||||||
|
__main__.py # CLI entry point
|
||||||
|
app.py # Application setup and routing
|
||||||
|
view/ # HTTP/WebSocket handlers
|
||||||
|
index.py # Landing page
|
||||||
|
rpc.py # WebSocket RPC
|
||||||
|
settings/ # Settings views
|
||||||
|
service/ # Business logic layer
|
||||||
|
mapper/ # Data persistence
|
||||||
|
model/ # Data models
|
||||||
|
form/ # Form definitions
|
||||||
|
system/ # Core infrastructure
|
||||||
|
view.py # BaseView class
|
||||||
|
service.py # BaseService class
|
||||||
|
mapper.py # BaseMapper class
|
||||||
|
model.py # BaseModel class
|
||||||
|
static/ # Frontend JavaScript and CSS
|
||||||
|
templates/ # Jinja2 templates</pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
655
src/snek/templates/site/bots.html
Normal file
655
src/snek/templates/site/bots.html
Normal file
@ -0,0 +1,655 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Bot Development - Snek Platform Documentation</title>
|
||||||
|
<meta name="description" content="Guide to developing bots for the Snek platform. Complete examples in Python and JavaScript for building automated chat bots." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section h3 {
|
||||||
|
color: #0fa;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section p, .section ul {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section ul {
|
||||||
|
list-style: disc inside;
|
||||||
|
}
|
||||||
|
.section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.section pre {
|
||||||
|
background: #222;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.section pre .kw { color: #f05a28; }
|
||||||
|
.section pre .fn { color: #7ef; }
|
||||||
|
.section pre .str { color: #0fa; }
|
||||||
|
.section pre .cm { color: #666; }
|
||||||
|
.code-label {
|
||||||
|
display: inline-block;
|
||||||
|
background: #f05a28;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.section pre { font-size: 0.7rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html" class="active">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<h1>Bot Development</h1>
|
||||||
|
<p>Build automated bots for the Snek platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="section">
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>Snek bots are automated clients that connect via WebSocket and interact with the platform using the RPC API. Bots can:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Monitor channels and respond to messages</li>
|
||||||
|
<li>Send automated notifications</li>
|
||||||
|
<li>Integrate with external services (CI/CD, monitoring, etc.)</li>
|
||||||
|
<li>Provide AI-powered assistance</li>
|
||||||
|
<li>Automate moderation tasks</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bots authenticate like regular users. Create a dedicated user account for your bot.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Protocol Overview</h2>
|
||||||
|
<p>Bots communicate over WebSocket using a JSON-RPC style protocol:</p>
|
||||||
|
|
||||||
|
<h3>Connection</h3>
|
||||||
|
<p>Connect to <code>wss://your-server/rpc.ws</code> (or <code>ws://</code> for local development).</p>
|
||||||
|
|
||||||
|
<h3>Request Format</h3>
|
||||||
|
<pre>{
|
||||||
|
"callId": "1",
|
||||||
|
"method": "login",
|
||||||
|
"args": ["bot_username", "bot_password"]
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>Response Format</h3>
|
||||||
|
<pre>{
|
||||||
|
"callId": "1",
|
||||||
|
"success": true,
|
||||||
|
"data": { "uid": "...", "username": "bot_username", ... }
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>Server Events</h3>
|
||||||
|
<p>After login, the server pushes events for new messages:</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "abc123",
|
||||||
|
"event": "new_message",
|
||||||
|
"data": {
|
||||||
|
"uid": "msg_uid",
|
||||||
|
"message": "Hello bot!",
|
||||||
|
"user_uid": "sender_uid",
|
||||||
|
"username": "sender_name"
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Python Bot Example</h2>
|
||||||
|
<p>A complete, working Python bot using aiohttp:</p>
|
||||||
|
|
||||||
|
<span class="code-label">Python</span>
|
||||||
|
<pre># retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
class SnekBot:
|
||||||
|
def __init__(self, base_url, username, password):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.ws = None
|
||||||
|
self.session = None
|
||||||
|
self.call_id = 0
|
||||||
|
self.pending = {}
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
self.session = aiohttp.ClientSession()
|
||||||
|
ws_url = self.base_url.replace("https", "wss").replace("http", "ws")
|
||||||
|
ws_url = f"{ws_url}/rpc.ws"
|
||||||
|
self.ws = await self.session.ws_connect(ws_url, heartbeat=30)
|
||||||
|
asyncio.create_task(self._message_loop())
|
||||||
|
user = await self.login()
|
||||||
|
print(f"Logged in as {user.get('username')}")
|
||||||
|
return user
|
||||||
|
|
||||||
|
async def _message_loop(self):
|
||||||
|
try:
|
||||||
|
async for msg in self.ws:
|
||||||
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||||
|
data = json.loads(msg.data)
|
||||||
|
call_id = data.get("callId")
|
||||||
|
if call_id and call_id in self.pending:
|
||||||
|
self.pending[call_id].set_result(data)
|
||||||
|
elif data.get("event") == "new_message":
|
||||||
|
await self.on_message(data.get("data", {}))
|
||||||
|
elif data.get("event"):
|
||||||
|
await self.on_event(data)
|
||||||
|
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Message loop error: {e}")
|
||||||
|
finally:
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def call(self, method, *args):
|
||||||
|
self.call_id += 1
|
||||||
|
call_id = str(self.call_id)
|
||||||
|
future = asyncio.get_event_loop().create_future()
|
||||||
|
self.pending[call_id] = future
|
||||||
|
await self.ws.send_json({
|
||||||
|
"callId": call_id,
|
||||||
|
"method": method,
|
||||||
|
"args": list(args)
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
result = await asyncio.wait_for(future, timeout=30)
|
||||||
|
return result.get("data")
|
||||||
|
finally:
|
||||||
|
self.pending.pop(call_id, None)
|
||||||
|
|
||||||
|
async def login(self):
|
||||||
|
return await self.call("login", self.username, self.password)
|
||||||
|
|
||||||
|
async def send_message(self, channel_uid, message):
|
||||||
|
return await self.call("send_message", channel_uid, message, True)
|
||||||
|
|
||||||
|
async def get_channels(self):
|
||||||
|
return await self.call("get_channels")
|
||||||
|
|
||||||
|
async def on_message(self, data):
|
||||||
|
message = data.get("message", "")
|
||||||
|
channel_uid = data.get("channel_uid")
|
||||||
|
username = data.get("username", "")
|
||||||
|
|
||||||
|
if username == self.username:
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.lower().startswith("!ping"):
|
||||||
|
await self.send_message(channel_uid, "Pong!")
|
||||||
|
|
||||||
|
elif message.lower().startswith("!help"):
|
||||||
|
help_text = "Available commands: !ping, !help, !time"
|
||||||
|
await self.send_message(channel_uid, help_text)
|
||||||
|
|
||||||
|
elif message.lower().startswith("!time"):
|
||||||
|
import datetime
|
||||||
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
await self.send_message(channel_uid, f"Current time: {now}")
|
||||||
|
|
||||||
|
async def on_event(self, data):
|
||||||
|
event = data.get("event")
|
||||||
|
print(f"Event: {event}")
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
self.running = False
|
||||||
|
if self.ws:
|
||||||
|
await self.ws.close()
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = SnekBot(
|
||||||
|
base_url="https://snek.community",
|
||||||
|
username="my_bot",
|
||||||
|
password="my_bot_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.connect()
|
||||||
|
channels = await bot.get_channels()
|
||||||
|
print(f"Member of {len(channels)} channels")
|
||||||
|
|
||||||
|
while bot.running:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Shutting down...")
|
||||||
|
finally:
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>JavaScript Bot Example</h2>
|
||||||
|
<p>A complete Node.js bot using the ws package:</p>
|
||||||
|
|
||||||
|
<span class="code-label">JavaScript (Node.js)</span>
|
||||||
|
<pre>// retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
const WebSocket = require("ws")
|
||||||
|
|
||||||
|
class SnekBot {
|
||||||
|
constructor(baseUrl, username, password) {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
this.ws = null
|
||||||
|
this.callId = 0
|
||||||
|
this.pending = new Map()
|
||||||
|
this.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wsUrl = this.baseUrl
|
||||||
|
.replace("https", "wss")
|
||||||
|
.replace("http", "ws") + "/rpc.ws"
|
||||||
|
|
||||||
|
this.ws = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
this.ws.on("open", async () => {
|
||||||
|
try {
|
||||||
|
const user = await this.login()
|
||||||
|
console.log(`Logged in as ${user.username}`)
|
||||||
|
resolve(user)
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on("message", (data) => {
|
||||||
|
const parsed = JSON.parse(data.toString())
|
||||||
|
const callId = parsed.callId
|
||||||
|
|
||||||
|
if (callId && this.pending.has(callId)) {
|
||||||
|
this.pending.get(callId).resolve(parsed)
|
||||||
|
this.pending.delete(callId)
|
||||||
|
} else if (parsed.event === "new_message") {
|
||||||
|
this.onMessage(parsed.data || {})
|
||||||
|
} else if (parsed.event) {
|
||||||
|
this.onEvent(parsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on("close", () => {
|
||||||
|
this.running = false
|
||||||
|
console.log("Connection closed")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.on("error", (err) => {
|
||||||
|
console.error("WebSocket error:", err.message)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
call(method, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.callId++
|
||||||
|
const callId = String(this.callId)
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this.pending.delete(callId)
|
||||||
|
reject(new Error("Request timeout"))
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
this.pending.set(callId, {
|
||||||
|
resolve: (data) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
resolve(data.data)
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
callId,
|
||||||
|
method,
|
||||||
|
args
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
return this.call("login", this.username, this.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(channelUid, message) {
|
||||||
|
return this.call("send_message", channelUid, message, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
getChannels() {
|
||||||
|
return this.call("get_channels")
|
||||||
|
}
|
||||||
|
|
||||||
|
async onMessage(data) {
|
||||||
|
const message = data.message || ""
|
||||||
|
const channelUid = data.channel_uid
|
||||||
|
const username = data.username || ""
|
||||||
|
|
||||||
|
if (username === this.username) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.toLowerCase().startsWith("!ping")) {
|
||||||
|
await this.sendMessage(channelUid, "Pong!")
|
||||||
|
} else if (message.toLowerCase().startsWith("!help")) {
|
||||||
|
await this.sendMessage(channelUid, "Commands: !ping, !help, !time")
|
||||||
|
} else if (message.toLowerCase().startsWith("!time")) {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
await this.sendMessage(channelUid, `Current time: ${now}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(data) {
|
||||||
|
console.log(`Event: ${data.event}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.running = false
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const bot = new SnekBot(
|
||||||
|
"https://snek.community",
|
||||||
|
"my_bot",
|
||||||
|
"my_bot_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bot.connect()
|
||||||
|
const channels = await bot.getChannels()
|
||||||
|
console.log(`Member of ${channels.length} channels`)
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.log("Shutting down...")
|
||||||
|
bot.close()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to connect:", e.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Event Handling</h2>
|
||||||
|
<p>Bots receive various events after authentication:</p>
|
||||||
|
|
||||||
|
<h3>new_message</h3>
|
||||||
|
<p>Received when a message is posted to a channel the bot is a member of.</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "...",
|
||||||
|
"event": "new_message",
|
||||||
|
"data": {
|
||||||
|
"uid": "message_uid",
|
||||||
|
"message": "Message content",
|
||||||
|
"user_uid": "sender_uid",
|
||||||
|
"username": "sender_username",
|
||||||
|
"nick": "Sender Nick",
|
||||||
|
"created_at": "2025-01-01T12:00:00"
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>set_typing</h3>
|
||||||
|
<p>Received when a user starts typing.</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "...",
|
||||||
|
"event": "set_typing",
|
||||||
|
"data": {
|
||||||
|
"user_uid": "...",
|
||||||
|
"username": "...",
|
||||||
|
"nick": "...",
|
||||||
|
"color": "#7ef"
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>update_message_text</h3>
|
||||||
|
<p>Received when a message is edited (non-final messages only).</p>
|
||||||
|
<pre>{
|
||||||
|
"channel_uid": "...",
|
||||||
|
"event": "update_message_text",
|
||||||
|
"data": {
|
||||||
|
"message_uid": "...",
|
||||||
|
"text": "Updated message content"
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>user_presence</h3>
|
||||||
|
<p>Received when a user's online status changes.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Best Practices</h2>
|
||||||
|
|
||||||
|
<h3>Rate Limiting</h3>
|
||||||
|
<p>Implement rate limiting to avoid overwhelming the server:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Limit outgoing messages to 1-2 per second per channel</li>
|
||||||
|
<li>Batch multiple operations where possible</li>
|
||||||
|
<li>Add delays between bulk operations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Error Handling</h3>
|
||||||
|
<p>Handle errors gracefully:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Catch and log all exceptions</li>
|
||||||
|
<li>Handle authentication failures specifically</li>
|
||||||
|
<li>Validate input before sending to the API</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Reconnection Strategy</h3>
|
||||||
|
<p>Implement automatic reconnection:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Detect disconnection via heartbeat timeout</li>
|
||||||
|
<li>Use exponential backoff (1s, 2s, 4s, 8s, max 60s)</li>
|
||||||
|
<li>Re-authenticate after reconnection</li>
|
||||||
|
<li>Restore subscriptions after login</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Resource Cleanup</h3>
|
||||||
|
<p>Clean up resources on shutdown:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Close WebSocket connections properly</li>
|
||||||
|
<li>Cancel pending timers and promises</li>
|
||||||
|
<li>Handle SIGINT/SIGTERM signals</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Security</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Store credentials in environment variables, not code</li>
|
||||||
|
<li>Use a dedicated bot account with minimal permissions</li>
|
||||||
|
<li>Validate and sanitize user input before processing</li>
|
||||||
|
<li>Log actions for audit purposes</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Advanced Topics</h2>
|
||||||
|
|
||||||
|
<h3>AI Integration</h3>
|
||||||
|
<p>Integrate with AI services by calling external APIs when messages match certain patterns:</p>
|
||||||
|
<pre>async def on_message(self, data):
|
||||||
|
message = data.get("message", "")
|
||||||
|
if message.startswith("!ask "):
|
||||||
|
query = message[5:]
|
||||||
|
response = await self.call_ai_api(query)
|
||||||
|
await self.send_message(data["channel_uid"], response)</pre>
|
||||||
|
|
||||||
|
<h3>Scheduled Tasks</h3>
|
||||||
|
<p>Use asyncio tasks for periodic operations:</p>
|
||||||
|
<pre>async def daily_report(self):
|
||||||
|
while self.running:
|
||||||
|
await asyncio.sleep(86400) # 24 hours
|
||||||
|
for channel in await self.get_channels():
|
||||||
|
await self.send_message(channel["uid"], "Daily report...")</pre>
|
||||||
|
|
||||||
|
<h3>Multiple Bots</h3>
|
||||||
|
<p>Run multiple bot instances for different purposes. Each bot needs its own user account and WebSocket connection.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
444
src/snek/templates/site/contribute.html
Normal file
444
src/snek/templates/site/contribute.html
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Contribute - Snek Platform Documentation</title>
|
||||||
|
<meta name="description" content="Guide to contributing to the Snek platform. Learn the project structure, code standards, and how to add new features." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section h3 {
|
||||||
|
color: #0fa;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section p, .section ul, .section ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section ul {
|
||||||
|
list-style: disc inside;
|
||||||
|
}
|
||||||
|
.section ol {
|
||||||
|
list-style: decimal inside;
|
||||||
|
}
|
||||||
|
.section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.section pre {
|
||||||
|
background: #222;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #7ef;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.dir-tree {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.dir-tree .dir { color: #7ef; }
|
||||||
|
.dir-tree .file { color: #aaa; }
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html" class="active">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<h1>Contribute</h1>
|
||||||
|
<p>How to contribute to the Snek platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="section">
|
||||||
|
<h2>Getting Started</h2>
|
||||||
|
|
||||||
|
<h3>Clone the Repository</h3>
|
||||||
|
<pre>git clone https://retoor.molodetz.nl/retoor/snek.git
|
||||||
|
cd snek</pre>
|
||||||
|
|
||||||
|
<h3>Setup Development Environment</h3>
|
||||||
|
<pre>make install</pre>
|
||||||
|
<p>This creates a virtual environment and installs all dependencies.</p>
|
||||||
|
|
||||||
|
<h3>Run Locally</h3>
|
||||||
|
<pre>make run</pre>
|
||||||
|
<p>The server starts at <code>http://localhost:8080</code> by default.</p>
|
||||||
|
|
||||||
|
<h3>Run Tests</h3>
|
||||||
|
<pre>pytest tests/</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Project Structure</h2>
|
||||||
|
|
||||||
|
<div class="dir-tree">
|
||||||
|
<span class="dir">src/snek/</span><br>
|
||||||
|
<span class="file">__main__.py</span> CLI entry point<br>
|
||||||
|
<span class="file">app.py</span> Application setup, routing<br>
|
||||||
|
<span class="dir">view/</span> HTTP/WebSocket handlers<br>
|
||||||
|
<span class="file">index.py</span> Landing page<br>
|
||||||
|
<span class="file">rpc.py</span> WebSocket RPC<br>
|
||||||
|
<span class="dir">settings/</span> Settings views<br>
|
||||||
|
<span class="dir">service/</span> Business logic layer<br>
|
||||||
|
<span class="file">user.py</span> User service<br>
|
||||||
|
<span class="file">channel.py</span> Channel service<br>
|
||||||
|
<span class="file">chat.py</span> Chat service<br>
|
||||||
|
<span class="dir">mapper/</span> Data persistence<br>
|
||||||
|
<span class="dir">model/</span> Data models<br>
|
||||||
|
<span class="dir">form/</span> Form definitions<br>
|
||||||
|
<span class="dir">system/</span> Core infrastructure<br>
|
||||||
|
<span class="file">view.py</span> BaseView class<br>
|
||||||
|
<span class="file">service.py</span> BaseService class<br>
|
||||||
|
<span class="file">mapper.py</span> BaseMapper class<br>
|
||||||
|
<span class="file">model.py</span> BaseModel class<br>
|
||||||
|
<span class="dir">static/</span> Frontend JS and CSS<br>
|
||||||
|
<span class="file">app.js</span> Application singleton<br>
|
||||||
|
<span class="file">socket.js</span> WebSocket client<br>
|
||||||
|
<span class="dir">templates/</span> Jinja2 templates<br>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Code Standards</h2>
|
||||||
|
|
||||||
|
<h3>Author Attribution</h3>
|
||||||
|
<p>Every file must include the author tag at the top:</p>
|
||||||
|
<pre># retoor <retoor@molodetz.nl></pre>
|
||||||
|
<p>For JavaScript:</p>
|
||||||
|
<pre>// retoor <retoor@molodetz.nl></pre>
|
||||||
|
|
||||||
|
<h3>No Comments</h3>
|
||||||
|
<p>Code should be self-documenting. If you need to explain something, refactor the code to be clearer. Comments indicate code that needs improvement.</p>
|
||||||
|
|
||||||
|
<h3>No JavaScript Frameworks</h3>
|
||||||
|
<p>The frontend uses vanilla ES6 JavaScript only. No React, Vue, Angular, or similar frameworks. Use Custom Elements for component encapsulation.</p>
|
||||||
|
|
||||||
|
<h3>Explicit Over Implicit</h3>
|
||||||
|
<p>Prefer explicit code paths. No magic imports, no hidden side effects, no implicit type conversions.</p>
|
||||||
|
|
||||||
|
<h3>Variable Naming</h3>
|
||||||
|
<p>Use clear, descriptive names. Avoid abbreviations except for common conventions (uid, url, etc.). No suffixes like <code>_super</code> or <code>_refactored</code>.</p>
|
||||||
|
|
||||||
|
<h3>Module Organization</h3>
|
||||||
|
<p>JavaScript: one class per file. Python: related classes can share a file, but keep files focused. Use directories to group related modules.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Adding a New View</h2>
|
||||||
|
<p>Views handle HTTP requests and render responses.</p>
|
||||||
|
|
||||||
|
<h3>1. Create the View Class</h3>
|
||||||
|
<pre># retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class MyFeatureView(BaseView):
|
||||||
|
login_required = True
|
||||||
|
|
||||||
|
async def get(self):
|
||||||
|
data = await self.services.my_service.get_data()
|
||||||
|
return await self.render_template("my_feature.html", {"data": data})</pre>
|
||||||
|
|
||||||
|
<h3>2. Create the Template</h3>
|
||||||
|
<p>Add a template at <code>templates/my_feature.html</code>:</p>
|
||||||
|
<pre>{% raw %}{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}My Feature{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="container">
|
||||||
|
<h1>My Feature</h1>
|
||||||
|
<p>{{ data }}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}{% endraw %}</pre>
|
||||||
|
|
||||||
|
<h3>3. Register the Route</h3>
|
||||||
|
<p>In <code>app.py</code> setup_router method:</p>
|
||||||
|
<pre>from snek.view.my_feature import MyFeatureView
|
||||||
|
|
||||||
|
self.router.add_view("/my-feature.html", MyFeatureView)</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Adding a New Service</h2>
|
||||||
|
<p>Services contain business logic and are accessed via <code>app.services.name</code>.</p>
|
||||||
|
|
||||||
|
<h3>1. Create the Service Class</h3>
|
||||||
|
<pre># retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from snek.system.service import BaseService
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(BaseService):
|
||||||
|
mapper_class = None
|
||||||
|
|
||||||
|
async def process_data(self, data):
|
||||||
|
return {"processed": data}</pre>
|
||||||
|
|
||||||
|
<h3>2. Register the Service</h3>
|
||||||
|
<p>Services are auto-discovered from the <code>service/</code> directory. The service name is derived from the filename (e.g., <code>my_service.py</code> becomes <code>app.services.my_service</code>).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Adding RPC Methods</h2>
|
||||||
|
<p>RPC methods are defined in the <code>RPCApi</code> class in <code>view/rpc.py</code>.</p>
|
||||||
|
|
||||||
|
<h3>Add the Method</h3>
|
||||||
|
<pre>async def my_method(self, param1, param2=None):
|
||||||
|
self._require_login()
|
||||||
|
self._require_services()
|
||||||
|
|
||||||
|
if not param1 or not isinstance(param1, str):
|
||||||
|
raise ValueError("Invalid param1")
|
||||||
|
|
||||||
|
result = await self.services.my_service.process(param1, param2)
|
||||||
|
return result</pre>
|
||||||
|
|
||||||
|
<h3>Method Guidelines</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Use <code>self._require_login()</code> for authenticated methods</li>
|
||||||
|
<li>Use <code>self._require_services()</code> to ensure services are available</li>
|
||||||
|
<li>Validate all input parameters</li>
|
||||||
|
<li>Return serializable data (dicts, lists, primitives)</li>
|
||||||
|
<li>Raise <code>ValueError</code> for invalid input</li>
|
||||||
|
<li>Raise <code>PermissionError</code> for authorization failures</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Adding Frontend Components</h2>
|
||||||
|
<p>Frontend components use Custom Elements.</p>
|
||||||
|
|
||||||
|
<h3>1. Create the Component</h3>
|
||||||
|
<pre>// retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
class MyComponent extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.data = null
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.innerHTML = `
|
||||||
|
<div class="my-component">
|
||||||
|
<h2>My Component</h2>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("my-component", MyComponent)
|
||||||
|
|
||||||
|
export { MyComponent }</pre>
|
||||||
|
|
||||||
|
<h3>2. Create the CSS</h3>
|
||||||
|
<p>Add styles in a separate CSS file (no Shadow DOM):</p>
|
||||||
|
<pre>.my-component {
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-component h2 {
|
||||||
|
color: #7ef;
|
||||||
|
}</pre>
|
||||||
|
|
||||||
|
<h3>3. Import in the Template</h3>
|
||||||
|
<pre><script type="module" src="/my-component.js"></script>
|
||||||
|
<link rel="stylesheet" href="/my-component.css"></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Testing</h2>
|
||||||
|
|
||||||
|
<h3>Running Tests</h3>
|
||||||
|
<pre>pytest tests/ # All tests
|
||||||
|
pytest tests/test_user.py # Single file
|
||||||
|
pytest tests/test_user.py::test_login # Single test
|
||||||
|
pytest -m unit # Unit tests only
|
||||||
|
pytest -m integration # Integration tests</pre>
|
||||||
|
|
||||||
|
<h3>Writing Tests</h3>
|
||||||
|
<pre># retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
async def test_my_feature():
|
||||||
|
result = await my_function()
|
||||||
|
assert result is not None</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Pull Request Guidelines</h2>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Create a feature branch from <code>main</code></li>
|
||||||
|
<li>Make focused, atomic commits</li>
|
||||||
|
<li>Write clear commit messages</li>
|
||||||
|
<li>Ensure all tests pass</li>
|
||||||
|
<li>Follow the code standards</li>
|
||||||
|
<li>Update documentation if needed</li>
|
||||||
|
<li>Submit PR with description of changes</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Commit Message Format</h3>
|
||||||
|
<pre>feat: add user profile editing
|
||||||
|
fix: resolve channel subscription issue
|
||||||
|
docs: update API documentation
|
||||||
|
refactor: simplify message handling</pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
320
src/snek/templates/site/design.html
Normal file
320
src/snek/templates/site/design.html
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Design Decisions - Snek Platform Documentation</title>
|
||||||
|
<meta name="description" content="Design philosophy and technical decisions behind the Snek platform. Learn why we chose specific technologies and architectural patterns." />
|
||||||
|
<link rel="stylesheet" href="/sandbox.css" />
|
||||||
|
<style>
|
||||||
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI',sans-serif;
|
||||||
|
background: #111;
|
||||||
|
color: #eee;
|
||||||
|
line-height:1.5;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
a { color: #7ef; text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.container { width: 90%; max-width: 960px; margin: auto; padding: 2rem 0; }
|
||||||
|
.page-hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
.page-hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
background: linear-gradient(90deg,#7ef 0%,#0fa 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
.page-hero p {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background: #181818;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.section h2 {
|
||||||
|
color: #7ef;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
.section h3 {
|
||||||
|
color: #0fa;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.section p, .section ul {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.section ul {
|
||||||
|
list-style: disc inside;
|
||||||
|
}
|
||||||
|
.section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.section code {
|
||||||
|
background: #222;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #7ef;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
.decision-box {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-left: 3px solid #f05a28;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
.decision-box strong {
|
||||||
|
color: #f05a28;
|
||||||
|
}
|
||||||
|
.topnav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
}
|
||||||
|
.topnav .nav-container {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
color: #7ef;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.14s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.topnav a:hover, .topnav a.active {
|
||||||
|
background: #222b;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.topnav .home-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
.topnav .home-link img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.4rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: auto;
|
||||||
|
box-shadow: 0 -1px 8px rgba(0,0,0,0.22);
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-hero h1 { font-size: 2rem; }
|
||||||
|
.section { padding: 1rem; }
|
||||||
|
.topnav .nav-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.topnav a {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="topnav">
|
||||||
|
<div class="nav-container">
|
||||||
|
<a class="home-link" href="/">
|
||||||
|
<img src="/image/snek_logo_256x256.png" alt="Snek" />
|
||||||
|
Snek
|
||||||
|
</a>
|
||||||
|
<a href="/docs.html">Docs</a>
|
||||||
|
<a href="/architecture.html">Architecture</a>
|
||||||
|
<a href="/api.html">API</a>
|
||||||
|
<a href="/bots.html">Bots</a>
|
||||||
|
<a href="/contribute.html">Contribute</a>
|
||||||
|
<a href="/about.html">About</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<header class="container page-hero">
|
||||||
|
<h1>Design Decisions</h1>
|
||||||
|
<p>Philosophy and technical choices that shape the Snek platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="section">
|
||||||
|
<h2>Core Philosophy</h2>
|
||||||
|
<p>Snek is built on a set of guiding principles that inform every technical and design decision:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Privacy First:</strong> No tracking, no analytics, no email required. User data belongs to users.</li>
|
||||||
|
<li><strong>Performance Focus:</strong> Fast is a feature. Minimal dependencies, optimized code paths.</li>
|
||||||
|
<li><strong>Simplicity Over Complexity:</strong> Prefer straightforward solutions. Complexity must justify itself.</li>
|
||||||
|
<li><strong>Self-Hosting First:</strong> The platform should be trivial to deploy and maintain independently.</li>
|
||||||
|
<li><strong>No Framework Lock-in:</strong> Avoid dependencies that impose architectural constraints.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Technical Decisions</h2>
|
||||||
|
|
||||||
|
<h3>Why aiohttp Over Flask/Django</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Use aiohttp as the web framework.
|
||||||
|
</div>
|
||||||
|
<p>Rationale:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Native async/await support without ASGI adapters</li>
|
||||||
|
<li>Built-in WebSocket support with no additional libraries</li>
|
||||||
|
<li>Lower memory footprint than Django</li>
|
||||||
|
<li>No ORM assumptions or admin interface overhead</li>
|
||||||
|
<li>Direct control over request/response lifecycle</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Why Dataset ORM Over SQLAlchemy</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Use Dataset for database access.
|
||||||
|
</div>
|
||||||
|
<p>Rationale:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Zero configuration required for basic operations</li>
|
||||||
|
<li>Dictionary-based interface matches JSON API patterns</li>
|
||||||
|
<li>Automatic table creation simplifies development</li>
|
||||||
|
<li>Raw SQL available when needed for complex queries</li>
|
||||||
|
<li>Significantly less boilerplate than SQLAlchemy models</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Why Vanilla JavaScript Over React/Vue</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Use vanilla ES6 JavaScript with no frameworks.
|
||||||
|
</div>
|
||||||
|
<p>Rationale:</p>
|
||||||
|
<ul>
|
||||||
|
<li>No build step required - direct browser execution</li>
|
||||||
|
<li>No framework version upgrades or breaking changes</li>
|
||||||
|
<li>Smaller payload - no framework runtime overhead</li>
|
||||||
|
<li>Easier debugging - no virtual DOM abstraction</li>
|
||||||
|
<li>Custom Elements provide component encapsulation</li>
|
||||||
|
<li>Direct DOM access for performance-critical paths</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Why SQLite as Default Database</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Use SQLite as the default database.
|
||||||
|
</div>
|
||||||
|
<p>Rationale:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Zero configuration deployment - single file database</li>
|
||||||
|
<li>No separate database server process required</li>
|
||||||
|
<li>Excellent read performance for chat workloads</li>
|
||||||
|
<li>Easy backup - copy a single file</li>
|
||||||
|
<li>PostgreSQL available for high-write scenarios</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Security Decisions</h2>
|
||||||
|
|
||||||
|
<h3>No IP Logging</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Do not log IP addresses or user activity.
|
||||||
|
</div>
|
||||||
|
<p>Snek does not record IP addresses, request logs, or user behavior patterns. This is a fundamental privacy commitment, not a configuration option.</p>
|
||||||
|
|
||||||
|
<h3>No Email Required</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Registration requires only username and password.
|
||||||
|
</div>
|
||||||
|
<p>Email is optional. Users can register and participate without providing any personally identifiable information. Password recovery relies on administrator assistance.</p>
|
||||||
|
|
||||||
|
<h3>Session Handling</h3>
|
||||||
|
<p>Sessions use secure, HTTP-only cookies with server-side storage. Session data is encrypted and expires after configurable inactivity periods.</p>
|
||||||
|
|
||||||
|
<h3>Password Storage</h3>
|
||||||
|
<p>Passwords are hashed using SHA-256 with a configurable salt. The plain password is never stored or logged.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>UI/UX Decisions</h2>
|
||||||
|
|
||||||
|
<h3>Dark Theme Default</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Use dark theme as the default and primary design.
|
||||||
|
</div>
|
||||||
|
<p>The dark theme reduces eye strain for extended use, is preferred by developers, and provides better contrast for code display.</p>
|
||||||
|
|
||||||
|
<h3>Card-Based Layouts</h3>
|
||||||
|
<p>Content is organized in cards with consistent padding, border-radius, and shadow. Cards provide visual grouping and work well across screen sizes.</p>
|
||||||
|
|
||||||
|
<h3>Minimal Animation</h3>
|
||||||
|
<p>Animations are limited to subtle transitions (hover states, menu opens). No gratuitous motion that could distract or slow down interaction.</p>
|
||||||
|
|
||||||
|
<h3>Typography</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Primary font: Segoe UI (system font stack fallback)</li>
|
||||||
|
<li>Code font: Courier New (monospace)</li>
|
||||||
|
<li>Base size: 16px with 1.5 line height</li>
|
||||||
|
<li>Color palette: #eee (text), #7ef (links/accents), #0fa (secondary)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>No Component Shadow DOM</h3>
|
||||||
|
<div class="decision-box">
|
||||||
|
<strong>Decision:</strong> Custom Elements use light DOM with separate CSS files.
|
||||||
|
</div>
|
||||||
|
<p>Each component has its own CSS file that integrates with the global stylesheet. This allows consistent theming and easier debugging compared to Shadow DOM isolation.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Code Standards</h2>
|
||||||
|
|
||||||
|
<h3>No Comments</h3>
|
||||||
|
<p>Code should be self-documenting through clear naming and structure. Comments indicate code that needs refactoring.</p>
|
||||||
|
|
||||||
|
<h3>Author Attribution</h3>
|
||||||
|
<p>Every file includes the author tag at the top:</p>
|
||||||
|
<code># retoor <retoor@molodetz.nl></code>
|
||||||
|
|
||||||
|
<h3>Modular Organization</h3>
|
||||||
|
<p>Code is organized into small, focused modules. One class per file for JavaScript. Related functionality grouped in directories.</p>
|
||||||
|
|
||||||
|
<h3>Explicit Over Implicit</h3>
|
||||||
|
<p>Prefer explicit code paths over magic behavior. No auto-imports, no implicit conversions, no hidden side effects.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Snek Platform - retoor <retoor@molodetz.nl></p>
|
||||||
|
</footer>
|
||||||
|
{% include "sandbox.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
src/snek/view/site.py
Normal file
28
src/snek/view/site.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
|
from snek.system.view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
class ArchitectureView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("site/architecture.html")
|
||||||
|
|
||||||
|
|
||||||
|
class DesignView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("site/design.html")
|
||||||
|
|
||||||
|
|
||||||
|
class ApiView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("site/api.html")
|
||||||
|
|
||||||
|
|
||||||
|
class BotsView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("site/bots.html")
|
||||||
|
|
||||||
|
|
||||||
|
class ContributeView(BaseView):
|
||||||
|
async def get(self):
|
||||||
|
return await self.render_template("site/contribute.html")
|
||||||
Loading…
Reference in New Issue
Block a user