Compare commits

...

32 Commits

Author SHA1 Message Date
f0e68cb31e Update. 2025-10-02 16:28:38 +02:00
8fe372532b Update. 2025-09-30 19:54:39 +02:00
214ac33049 Update. 2025-09-30 19:47:17 +02:00
7b245869d5 Upate cache. 2025-09-30 19:35:56 +02:00
f5a2928f1b Update, cleanup 2025-09-30 19:28:59 +02:00
f74146bb11 Update cache. 2025-09-30 19:27:07 +02:00
f6157bf879 Update cache. 2025-09-30 19:14:16 +02:00
704adf6fe8 Update asyncio 2025-09-30 17:36:22 +02:00
f2575e04fb Update. 2025-09-30 17:20:02 +02:00
ba7bbbfc62 Update. 2025-09-30 17:18:48 +02:00
1db9947267 Update. 2025-09-30 17:17:06 +02:00
38d05da272 Update logging info. 2025-09-22 14:15:53 +02:00
c740de95f9 Update. 2025-09-22 14:14:33 +02:00
483a63ede9 Update. 2025-09-22 13:30:53 +02:00
a817b9b61d Update. 2025-09-17 22:55:31 +02:00
0c6da9845c Update. 2025-09-17 16:30:20 +02:00
586c653e4f Update. 2025-09-17 16:27:04 +02:00
c8cc411aa5 Min distance. 2025-09-17 16:20:59 +02:00
71d967114d Added new views. 2025-09-17 16:12:05 +02:00
659c30f376 Update. 2025-09-17 16:09:48 +02:00
70d3b3b019 REconnect delay. 2025-09-16 04:46:44 +02:00
0b8c324944 Update. 2025-09-15 05:00:11 +02:00
cbbaa22f56 Update. 2025-09-15 02:22:03 +02:00
fb754cad92 Update. 2025-09-15 02:12:52 +02:00
0518cdce0b Update. 2025-09-15 02:09:04 +02:00
1af739cac2 Update. 2025-09-15 00:58:29 +02:00
b28ba3c47d Update 2025-09-14 22:53:05 +02:00
fadc57a7c7 Update. 2025-09-09 23:40:45 +02:00
cca3946a35 Update channel message. 2025-09-08 06:08:12 +02:00
18be3fdc19 Executor pools. 2025-09-08 01:09:22 +02:00
939e63f244 Executor pools. 2025-09-08 00:59:11 +02:00
b4c267d584 Update. 2025-09-07 02:42:47 +02:00
21 changed files with 445 additions and 740 deletions

View File

@ -1,3 +1,7 @@
import logging
logging.basicConfig(level=logging.INFO)
import pathlib
import shutil
import sqlite3

View File

@ -14,6 +14,9 @@ from snek.view.threads import ThreadsView
logging.basicConfig(level=logging.DEBUG)
from concurrent.futures import ThreadPoolExecutor
from ipaddress import ip_address
import time
import uuid
import IP2Location
from aiohttp import web
@ -32,7 +35,6 @@ from snek.sgit import GitApplication
from snek.sssh import start_ssh_server
from snek.system import http
from snek.system.cache import Cache
from snek.system.stats import middleware as stats_middleware, create_stats_structure, stats_handler
from snek.system.markdown import MarkdownExtension
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
from snek.system.profiler import profiler_handler
@ -42,6 +44,7 @@ from snek.system.template import (
PythonExtension,
sanitize_html,
)
from snek.view.new import NewView
from snek.view.about import AboutHTMLView, AboutMDView
from snek.view.avatar import AvatarView
from snek.view.channel import ChannelAttachmentView,ChannelAttachmentUploadView, ChannelView
@ -129,10 +132,7 @@ async def trailing_slash_middleware(request, handler):
class Application(BaseApplication):
def __init__(self, *args, **kwargs):
middlewares = [
stats_middleware,
cors_middleware,
web.normalize_path_middleware(merge_slashes=True),
ip2location_middleware,
csp_middleware,
]
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
@ -171,19 +171,12 @@ class Application(BaseApplication):
self.ip2location = IP2Location.IP2Location(
base_path.joinpath("IP2LOCATION-LITE-DB11.BIN")
)
self.on_startup.append(self.prepare_stats)
self.on_startup.append(self.prepare_asyncio)
self.on_startup.append(self.start_user_availability_service)
self.on_startup.append(self.start_ssh_server)
self.on_startup.append(self.prepare_database)
async def prepare_stats(self, app):
app['stats'] = create_stats_structure()
print("Stats prepared", flush=True)
@property
def uptime_seconds(self):
return (datetime.now() - self.time_start).total_seconds()
@ -272,6 +265,8 @@ class Application(BaseApplication):
name="static",
show_index=True,
)
self.router.add_view("/new.html", NewView)
self.router.add_view("/profiler.html", profiler_handler)
self.router.add_view("/container/sock/{channel_uid}.json", ContainerView)
self.router.add_view("/about.html", AboutHTMLView)
@ -319,7 +314,6 @@ class Application(BaseApplication):
#self.router.add_view("/drive.json", DriveApiView)
#self.router.add_view("/drive.html", DriveView)
#self.router.add_view("/drive/{drive}.json", DriveView)
self.router.add_get("/stats.html", stats_handler)
self.router.add_view("/stats.json", StatsView)
self.router.add_view("/user/{user}.html", UserView)
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
@ -374,6 +368,8 @@ class Application(BaseApplication):
# @time_cache_async(60)
async def render_template(self, template, request, context=None):
start_time = time.perf_counter()
channels = []
if not context:
context = {}
@ -434,10 +430,12 @@ class Application(BaseApplication):
self.jinja2_env.loader = self.original_loader
end_time = time.perf_counter()
print(f"render_template took {end_time - start_time:.4f} seconds")
# rendered.text = whitelist_attributes(rendered.text)
# rendered.headers['Content-Lenght'] = len(rendered.text)
return rendered
async def static_handler(self, request):
file_name = request.match_info.get("filename", "")

View File

@ -2,9 +2,19 @@ from snek.system.service import BaseService
from snek.system.template import sanitize_html
import time
import asyncio
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import json
from jinja2 import Environment, FileSystemLoader
global jinja2_env
import pathlib
template_path = pathlib.Path(__file__).parent.parent.joinpath("templates")
def render(context):
template =jinja2_env.get_template("message.html")
return sanitize_html(template.render(**context))
executor = ThreadPoolExecutor(max_workers=50)
class ChannelMessageService(BaseService):
@ -13,9 +23,26 @@ class ChannelMessageService(BaseService):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._configured_indexes = False
self._executor_pools = {}
global jinja2_env
jinja2_env = self.app.jinja2_env
self._max_workers = 1
def get_or_create_executor(self, uid):
if not uid in self._executor_pools:
self._executor_pools[uid] = ProcessPoolExecutor(max_workers=self._max_workers)
print("Executors available", len(self._executor_pools))
return self._executor_pools[uid]
def delete_executor(self, uid):
if uid in self._executor_pools:
self._executor_pools[uid].shutdown()
del self._executor_pools[uid]
async def maintenance(self):
args = {}
return
for message in self.mapper.db["channel_message"].find():
print(message)
try:
@ -76,9 +103,12 @@ class ChannelMessageService(BaseService):
)
loop = asyncio.get_event_loop()
try:
template = self.app.jinja2_env.get_template("message.html")
model["html"] = await loop.run_in_executor(executor, lambda: template.render(**context))
model['html'] = await loop.run_in_executor(executor, lambda:sanitize_html(model['html']))
context = json.loads(json.dumps(context, default=str))
model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render,context)
#model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html'])
except Exception as ex:
print(ex, flush=True)
@ -97,6 +127,8 @@ class ChannelMessageService(BaseService):
["deleted_at"], unique=False
)
self._configured_indexes = True
if model['is_final']:
self.delete_executor(model['uid'])
return model
raise Exception(f"Failed to create channel message: {model.errors}.")
@ -133,12 +165,15 @@ class ChannelMessageService(BaseService):
"color": user["color"],
}
)
template = self.app.jinja2_env.get_template("message.html")
context = json.loads(json.dumps(context, default=str))
loop = asyncio.get_event_loop()
model["html"] = await loop.run_in_executor(executor, lambda: template.render(**context))
model['html'] = await loop.run_in_executor(executor, lambda: sanitize_html(model['html']))
return await super().save(model)
model["html"] = await loop.run_in_executor(self.get_or_create_executor(model["uid"]), render, context)
#model['html'] = await loop.run_in_executor(self.get_or_create_executor(user["uid"]), sanitize_html,model['html'])
result = await super().save(model)
if model['is_final']:
self.delete_executor(model['uid'])
return result
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
channel = await self.services.channel.get(uid=channel_uid)

View File

@ -65,7 +65,7 @@ export class Chat extends EventHandler {
}
return new Promise((resolve) => {
this._waitConnect = resolve;
console.debug("Connecting..");
//console.debug("Connecting..");
try {
this._socket = new WebSocket(this._url);
@ -142,7 +142,7 @@ export class NotificationAudio {
new Audio(this.sounds[soundIndex])
.play()
.then(() => {
console.debug("Gave sound notification");
//console.debug("Gave sound notification");
})
.catch((error) => {
console.error("Notification failed:", error);

View File

@ -81,7 +81,152 @@ class ChatInputComponent extends NjetComponent {
return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), m => m[1]);
}
matchMentionsToAuthors(mentions, authors) {
return mentions.map((mention) => {
const lowerMention = mention.toLowerCase();
let bestMatch = null;
let bestScore = 0;
for (const author of authors) {
const lowerAuthor = author.toLowerCase();
let score = 0;
if (lowerMention === lowerAuthor) {
score = 100;
} else if (lowerAuthor.startsWith(lowerMention)) {
score = 90 + (5 * (lowerMention.length / lowerAuthor.length));
} else if (lowerAuthor.includes(lowerMention) && lowerMention.length >= 2) {
const position = lowerAuthor.indexOf(lowerMention);
score = 80 - (10 * (position / lowerAuthor.length));
} else if (this.isFuzzyMatch(lowerMention, lowerAuthor)) {
const ratio = lowerMention.length / lowerAuthor.length;
score = 40 + (20 * ratio);
} else if (this.isCloseMatch(lowerMention, lowerAuthor)) {
score = 30 + (10 * (lowerMention.length / lowerAuthor.length));
}
if (score > bestScore) {
bestScore = score;
bestMatch = author;
}
}
const minScore = 40;
return {
mention,
closestAuthor: bestScore >= minScore ? bestMatch : null,
score: bestScore,
};
});
}
isFuzzyMatch(needle, haystack) {
if (needle.length < 2) return false;
let needleIndex = 0;
for (let i = 0; i < haystack.length && needleIndex < needle.length; i++) {
if (haystack[i] === needle[needleIndex]) {
needleIndex++;
}
}
return needleIndex === needle.length;
}
isCloseMatch(str1, str2) {
if (Math.abs(str1.length - str2.length) > 2) return false;
const shorter = str1.length <= str2.length ? str1 : str2;
const longer = str1.length > str2.length ? str1 : str2;
let differences = 0;
let j = 0;
for (let i = 0; i < shorter.length && j < longer.length; i++) {
if (shorter[i] !== longer[j]) {
differences++;
if (j + 1 < longer.length && shorter[i] === longer[j + 1]) {
j++;
}
}
j++;
}
differences += Math.abs(longer.length - j);
return differences <= 2;
}
matchMentions4ToAuthors(mentions, authors) {
return mentions.map((mention) => {
let closestAuthor = null;
let minDistance = Infinity;
const lowerMention = mention.toLowerCase();
authors.forEach((author) => {
const lowerAuthor = author.toLowerCase();
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
if (!this.isSubsequence(lowerMention, lowerAuthor)) {
distance += 10;
}
if (distance < minDistance) {
minDistance = distance;
closestAuthor = author;
}
});
if (minDistance < 5) {
closestAuthor = 0;
}
return { mention, closestAuthor, distance: minDistance };
});
}
levenshteinDistance(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + 1
);
}
}
}
return matrix[b.length][a.length];
}
replaceMentionsWithAuthors(text) {
const authors = this.getAuthors();
const mentions = this.extractMentions(text);
const matches = this.matchMentionsToAuthors(mentions, authors);
let updatedText = text;
matches.forEach(({ mention, closestAuthor }) => {
if(closestAuthor){
const mentionRegex = new RegExp(`@${mention}`, 'g');
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
}
});
return updatedText;
}
matchMentions2ToAuthors(mentions, authors) {
return mentions.map(mention => {
let closestAuthor = null;
let minDistance = Infinity;
@ -100,53 +245,14 @@ class ChatInputComponent extends NjetComponent {
closestAuthor = author;
}
});
if (minDistance < 5){
closestAuthor = 0;
}
return { mention, closestAuthor, distance: minDistance };
});
}
levenshteinDistance(a, b) {
const matrix = [];
// Initialize the first row and column
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the matrix
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // Deletion
matrix[i][j - 1] + 1, // Insertion
matrix[i - 1][j - 1] + 1 // Substitution
);
}
}
}
return matrix[b.length][a.length];
}
replaceMentionsWithAuthors(text) {
const authors = this.getAuthors();
const mentions = this.extractMentions(text);
const matches = this.matchMentionsToAuthors(mentions, authors);
let updatedText = text;
matches.forEach(({ mention, closestAuthor }) => {
const mentionRegex = new RegExp(`@${mention}`, 'g');
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
});
return updatedText;
}
textToLeet(text) {
// L33t speak character mapping
const leetMap = {
@ -392,7 +498,7 @@ textToLeetAdvanced(text) {
}
j++;
}
return i === s.length;
return i === s.length && s.length > 1;
}
flagTyping() {

View File

@ -256,7 +256,9 @@ class MessageList extends HTMLElement {
this.querySelectorAll('.avatar').forEach((el) => {
const anchor = el.closest('a');
if (anchor && typeof anchor.href === 'string' && anchor.href.includes(uid)) {
if(!lastElement)
lastElement = el;
}
});
if (lastElement) {
@ -278,11 +280,15 @@ class MessageList extends HTMLElement {
upsertMessage(data) {
let message = this.messageMap.get(data.uid);
if (message && (data.is_final || !data.message)) {
message.parentElement?.removeChild(message);
//message.parentElement?.removeChild(message);
// TO force insert
message = null;
//message = null;
}
if(message && !data.message){
message.parentElement?.removeChild(message);
message = null;
}
if (!data.message) return;
const wrapper = document.createElement("div");

View File

@ -52,7 +52,7 @@ self.addEventListener("push", async (event) => {
data,
}).then(e => console.log("Showing notification", e)).catch(console.error);
event.waitUntil(reg);
// event.waitUntil(reg);
});

View File

@ -86,6 +86,7 @@ export class Socket extends EventHandler {
}
this.emit("data", data.data);
if (data["event"]) {
console.info([data.event,data.data])
this.emit(data.event, data.data);
}
}
@ -99,7 +100,7 @@ export class Socket extends EventHandler {
console.log("Reconnecting");
this.emit("reconnecting");
return this.connect();
}, 0);
}, 4000);
}
_camelToSnake(str) {

View File

@ -1,75 +1,137 @@
import asyncio
import functools
import json
from collections import OrderedDict
from snek.system import security
cache = functools.cache
CACHE_MAX_ITEMS_DEFAULT = 5000
class Cache:
"""
An asynchronous, thread-safe, in-memory LRU (Least Recently Used) cache.
This implementation uses an OrderedDict for efficient O(1) time complexity
for its core get, set, and delete operations.
"""
def __init__(self, app, max_items=CACHE_MAX_ITEMS_DEFAULT):
self.app = app
self.cache = {}
# OrderedDict is the core of the LRU logic. It remembers the order
# in which items were inserted.
self.cache: OrderedDict = OrderedDict()
self.max_items = max_items
self.stats = {}
self.enabled = True
self.lru = []
# A lock is crucial to prevent race conditions in an async environment.
self._lock = asyncio.Lock()
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
async def get(self, args):
async def get(self, key):
"""
Retrieves an item from the cache. If found, it's marked as recently used.
Returns None if the item is not found or the cache is disabled.
"""
if not self.enabled:
return None
await self.update_stat(args, "get")
try:
self.lru.pop(self.lru.index(args))
except:
# print("Cache miss!", args, flush=True)
#async with self._lock:
if key not in self.cache:
await self.update_stat(key, "get")
return None
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
# print("Cache hit!", args, flush=True)
return self.cache[args]
# Mark as recently used by moving it to the end of the OrderedDict.
# This is an O(1) operation.
self.cache.move_to_end(key)
await self.update_stat(key, "get")
return self.cache[key]
async def set(self, key, value):
"""
Adds or updates an item in the cache and marks it as recently used.
If the cache exceeds its maximum size, the least recently used item is evicted.
"""
if not self.enabled:
return
# comment
#async with self._lock:
is_new = key not in self.cache
# Add or update the item. If it exists, it's moved to the end.
self.cache[key] = value
self.cache.move_to_end(key)
await self.update_stat(key, "set")
# Evict the least recently used item if the cache is full.
# This is an O(1) operation.
if len(self.cache) > self.max_items:
# popitem(last=False) removes and returns the first (oldest) item.
evicted_key, _ = self.cache.popitem(last=False)
# Optionally, you could log the evicted key here.
if is_new:
self.version += 1
async def delete(self, key):
"""Removes an item from the cache if it exists."""
if not self.enabled:
return
async with self._lock:
if key in self.cache:
await self.update_stat(key, "delete")
# Deleting from OrderedDict is an O(1) operation on average.
del self.cache[key]
async def get_stats(self):
all_ = []
for key in self.lru:
all_.append(
{
"key": key,
"set": self.stats[key]["set"],
"get": self.stats[key]["get"],
"delete": self.stats[key]["delete"],
"value": str(self.serialize(self.cache[key].record)),
}
)
return all_
"""Returns statistics for all items currently in the cache."""
async with self._lock:
stats_list = []
# Items are iterated from oldest to newest. We reverse to show
# most recently used items first.
for key in reversed(self.cache):
stat_data = self.stats.get(key, {"set": 0, "get": 0, "delete": 0})
value = self.cache[key]
value_record = value.record if hasattr(value, 'record') else value
def serialize(self, obj):
cpy = obj.copy()
cpy.pop("created_at", None)
cpy.pop("deleted_at", None)
cpy.pop("email", None)
cpy.pop("password", None)
return cpy
stats_list.append({
"key": key,
"set": stat_data.get("set", 0),
"get": stat_data.get("get", 0),
"delete": stat_data.get("delete", 0),
"value": str(self.serialize(value_record)),
})
return stats_list
async def update_stat(self, key, action):
"""Updates hit/miss/set counts for a given cache key."""
# This method is already called within a locked context,
# but the lock makes it safe if ever called directly.
async with self._lock:
if key not in self.stats:
self.stats[key] = {"set": 0, "get": 0, "delete": 0}
self.stats[key][action] = self.stats[key][action] + 1
self.stats[key][action] += 1
def serialize(self, obj):
"""A synchronous helper to create a serializable representation of an object."""
if not isinstance(obj, dict):
return obj
cpy = obj.copy()
for key_to_remove in ["created_at", "deleted_at", "email", "password"]:
cpy.pop(key_to_remove, None)
return cpy
def json_default(self, value):
# if hasattr(value, "to_json"):
# return value.to_json()
"""JSON serializer fallback for objects that are not directly serializable."""
try:
return json.dumps(value.__dict__, default=str)
except:
return str(value)
async def create_cache_key(self, args, kwargs):
"""Creates a consistent, hashable cache key from function arguments."""
# security.hash is async, so this method remains async.
return await security.hash(
json.dumps(
{"args": args, "kwargs": kwargs},
@ -78,38 +140,8 @@ class Cache:
)
)
async def set(self, args, result):
if not self.enabled:
return
is_new = args not in self.cache
self.cache[args] = result
await self.update_stat(args, "set")
try:
self.lru.pop(self.lru.index(args))
except (ValueError, IndexError):
pass
self.lru.insert(0, args)
while len(self.lru) > self.max_items:
self.cache.pop(self.lru[-1])
self.lru.pop()
if is_new:
self.version += 1
# print(f"Cache store! {len(self.lru)} items. New version:", self.version, flush=True)
async def delete(self, args):
if not self.enabled:
return
await self.update_stat(args, "delete")
if args in self.cache:
try:
self.lru.pop(self.lru.index(args))
except IndexError:
pass
del self.cache[args]
def async_cache(self, func):
"""Decorator to cache the results of an async function."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
@ -119,33 +151,14 @@ class Cache:
result = await func(*args, **kwargs)
await self.set(cache_key, result)
return result
return wrapper
def async_delete_cache(self, func):
"""Decorator to invalidate a cache entry before running an async function."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
cache_key = await self.create_cache_key(args, kwargs)
if cache_key in self.cache:
try:
self.lru.pop(self.lru.index(cache_key))
except IndexError:
pass
del self.cache[cache_key]
await self.delete(cache_key)
return await func(*args, **kwargs)
return wrapper
def async_cache(func):
cache = {}
@functools.wraps(func)
async def wrapper(*args):
if args in cache:
return cache[args]
result = await func(*args)
cache[args] = result
return result
return wrapper

View File

@ -1,7 +1,7 @@
DEFAULT_LIMIT = 30
import asyncio
import typing
import time
from snek.system.model import BaseModel
@ -15,8 +15,6 @@ class BaseMapper:
def __init__(self, app):
self.app = app
self.default_limit = self.__class__.default_limit
@property
def db(self):
return self.app.db
@ -27,6 +25,7 @@ class BaseMapper:
async def run_in_executor(self, func, *args, **kwargs):
use_semaphore = kwargs.pop("use_semaphore", False)
start_time = time.time()
def _execute():
result = func(*args, **kwargs)
@ -34,10 +33,8 @@ class BaseMapper:
self.db.commit()
return result
return _execute()
#async with self.semaphore:
# return await self.loop.run_in_executor(None, _execute)
async with self.semaphore:
return await asyncio.to_thread(_execute)
async def new(self):
return self.model_class(mapper=self, app=self.app)

View File

@ -1,129 +0,0 @@
import asyncio
from aiohttp import web, WSMsgType
from datetime import datetime, timedelta, timezone
from collections import defaultdict
import html
def create_stats_structure():
"""Creates the nested dictionary structure for storing statistics."""
def nested_dd():
return defaultdict(lambda: defaultdict(int))
return defaultdict(nested_dd)
def get_time_keys(dt: datetime):
"""Generates dictionary keys for different time granularities."""
return {
"hour": dt.strftime('%Y-%m-%d-%H'),
"day": dt.strftime('%Y-%m-%d'),
"week": dt.strftime('%Y-%W'), # Week number, Monday is first day
"month": dt.strftime('%Y-%m'),
}
def update_stats_counters(stats_dict: defaultdict, now: datetime):
"""Increments the appropriate time-based counters in a stats dictionary."""
keys = get_time_keys(now)
stats_dict['by_hour'][keys['hour']] += 1
stats_dict['by_day'][keys['day']] += 1
stats_dict['by_week'][keys['week']] += 1
stats_dict['by_month'][keys['month']] += 1
def generate_time_series_svg(title: str, data: list[tuple[str, int]], y_label: str) -> str:
"""Generates a responsive SVG bar chart for time-series data."""
if not data:
return f"<h3>{html.escape(title)}</h3><p>No data yet.</p>"
max_val = max(item[1] for item in data) if data else 1
svg_height, svg_width = 250, 600
bar_padding = 5
bar_width = (svg_width - 50) / len(data) - bar_padding
bars = ""
labels = ""
for i, (key, val) in enumerate(data):
bar_height = (val / max_val) * (svg_height - 50) if max_val > 0 else 0
x = i * (bar_width + bar_padding) + 40
y = svg_height - bar_height - 30
bars += f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_height}" fill="#007BFF"><title>{html.escape(key)}: {val}</title></rect>'
labels += f'<text x="{x + bar_width / 2}" y="{svg_height - 15}" font-size="11" text-anchor="middle">{html.escape(key)}</text>'
return f"""
<h3>{html.escape(title)}</h3>
<div style="border:1px solid #ccc; padding: 10px; border-radius: 5px;">
<svg viewBox="0 0 {svg_width} {svg_height}" style="width:100%; height:auto;">
<g>{bars}</g>
<g>{labels}</g>
<line x1="35" y1="10" x2="35" y2="{svg_height - 30}" stroke="#aaa" stroke-width="1" />
<line x1="35" y1="{svg_height - 30}" x2="{svg_width - 10}" y2="{svg_height - 30}" stroke="#aaa" stroke-width="1" />
<text x="5" y="{svg_height - 30}" font-size="12">0</text>
<text x="5" y="20" font-size="12">{max_val}</text>
</svg>
</div>
"""
@web.middleware
async def middleware(request, handler):
"""Middleware to count all incoming HTTP requests."""
# Avoid counting requests to the stats page itself
if request.path.startswith('/stats.html'):
return await handler(request)
update_stats_counters(request.app['stats']['http_requests'], datetime.now(timezone.utc))
return await handler(request)
def update_websocket_stats(app):
update_stats_counters(app['stats']['websocket_requests'], datetime.now(timezone.utc))
async def pipe_and_count_websocket(ws_from, ws_to, stats_dict):
"""This function proxies WebSocket messages AND counts them."""
async for msg in ws_from:
# This is the key part for monitoring WebSockets
update_stats_counters(stats_dict, datetime.now(timezone.utc))
if msg.type == WSMsgType.TEXT:
await ws_to.send_str(msg.data)
elif msg.type == WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type in (WSMsgType.CLOSE, WSMsgType.ERROR):
await ws_to.close(code=ws_from.close_code)
break
async def stats_handler(request: web.Request):
"""Handler to display the statistics dashboard."""
stats = request.app['stats']
now = datetime.now(timezone.utc)
# Helper to prepare data for charts
def get_data(source, period, count):
data = []
for i in range(count - 1, -1, -1):
if period == 'hour':
dt = now - timedelta(hours=i)
key, label = dt.strftime('%Y-%m-%d-%H'), dt.strftime('%H:00')
data.append((label, source['by_hour'].get(key, 0)))
elif period == 'day':
dt = now - timedelta(days=i)
key, label = dt.strftime('%Y-%m-%d'), dt.strftime('%a')
data.append((label, source['by_day'].get(key, 0)))
return data
http_hourly = get_data(stats['http_requests'], 'hour', 24)
ws_hourly = get_data(stats['ws_messages'], 'hour', 24)
http_daily = get_data(stats['http_requests'], 'day', 7)
ws_daily = get_data(stats['ws_messages'], 'day', 7)
body = f"""
<html><head><title>App Stats</title><meta http-equiv="refresh" content="30"></head>
<body>
<h2>Application Dashboard</h2>
<h3>Last 24 Hours</h3>
{generate_time_series_svg("HTTP Requests", http_hourly, "Reqs/Hour")}
{generate_time_series_svg("WebSocket Messages", ws_hourly, "Msgs/Hour")}
<h3>Last 7 Days</h3>
{generate_time_series_svg("HTTP Requests", http_daily, "Reqs/Day")}
{generate_time_series_svg("WebSocket Messages", ws_daily, "Msgs/Day")}
</body></html>
"""
return web.Response(text=body, content_type='text/html')

View File

@ -32,7 +32,7 @@
<link rel="stylesheet" href="/base.css">
<link rel="icon" type="image/png" href="/image/snek_logo_32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/image/snek_logo_64x64.png" sizes="64x64">
<script nonce="{{nonce}}" defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea"></script>
<script nonce="{{nonce}}" defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea" defer></script>
</head>
<body>
<header>

View File

@ -0,0 +1,97 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
<link rel="manifest" href="/manifest.json" />
<style>
body{
background-color: black;
color: white;
}
</style>
</head>
<body>
<script type="module">
import { Socket } from "./socket.js";
class ChatWindow extends HTMLElement {
constructor() {
super();
this.component = document.createElement("div");
this.message_list = document.createElement("div")
this.component.appendChild(this.message_list);
this.chat_input = document.createElement("div")
this.component.appendChild(this.chat_input);
this.channelUid = null
this.channelUid = this.getAttribute("channel")
this.inputText = document.createElement("textarea")
this.inputText.addEventListener("keyup",(e)=>{
this.rpc.sendMessage(this.channelUid, e.target.value,false)
if(e.key == "Enter" && !e.shiftKey){
this.rpc.sendMessage(this.channelUid, e.target.value,true)
e.target.value = ""
}else{
//this.rpc.sendMessage(this.channelUid, e.target.value, false)
}
})
this.component.appendChild(this.inputText)
this.ws = new Socket();
this.ws.addEventListener("channel-message", this.handleMessage.bind(this))
this.rpc = this.ws.client
this.ws.addEventListener("update_message_text",this.handleMessage.bind(this))
window.chat = this
}
async handleMessage(data,data2) {
if(data2 && data2.event)
data = data.data
console.info(["update-messagettt",data])
console.warn(data.uid)
if(!data.html)
return
let div = this.message_list.querySelector('[data-uid="' + data.uid + '"]');
console.info(div)
if(!div){
let temp = document.createElement("chat-message");
temp.innerHTML = data.html
this.message_list.appendChild(temp)
//this.message_list.replace(div,temp)
//div.innerHTML = data.html
//this.message_list.appendChild(div);
}else{
// alert("HIERR")
let temp = document.createElement("chat-message");
temp.innerHTML = data.html;
div.innerHTML = temp.innerHTML
console.info("REPLACE")
}
}
async connectedCallback() {
await this.rpc.ping(this.channel)
console.info(this.channelUid)
this.messages = await this.rpc.getMessages(this.channelUid, 0, 0);
this.messages.forEach((msg) => {
const temp = document.createElement("div");
temp.innerHTML = msg.html;
this.message_list.appendChild(temp.firstChild);
})
this.appendChild(this.component);
}
}
customElements.define("chat-window", ChatWindow);
</script>
<chat-window channel="df3e1259-7d1a-4184-b75c-3befd5bf08e1"></chat-window>
</body>
</html>

View File

@ -71,7 +71,10 @@ class AvatarView(BaseView):
uid = self.request.match_info.get("uid")
if uid == "unique":
uid = str(uuid.uuid4())
avatar = await self.app.get(uid)
if not avatar:
avatar = multiavatar.multiavatar(uid, True, None)
await self.app.set(uid, avatar)
response = web.Response(text=avatar, content_type="image/svg+xml")
response.headers["Cache-Control"] = f"public, max-age={1337*42}"
return response

8
src/snek/view/new.py Normal file
View File

@ -0,0 +1,8 @@
from snek.system.view import BaseView
class NewView(BaseView):
login_required = True
async def get(self):
return await self.render_template("new.html")

View File

@ -6,7 +6,6 @@
# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
from snek.system.stats import update_websocket_stats
import asyncio
import json
import logging
@ -507,9 +506,7 @@ class RPCView(BaseView):
raise Exception("Method not found")
success = True
try:
update_websocket_stats(self.app)
result = await method(*args)
update_websocket_stats(self.app)
except Exception as ex:
result = {"exception": str(ex), "traceback": traceback.format_exc()}
success = False

View File

@ -1,78 +0,0 @@
import asyncio
import logging
import os
import asyncssh
asyncssh.set_debug_level(2)
logging.basicConfig(level=logging.DEBUG)
# Configuration for SFTP server
SFTP_ROOT = "." # Directory to serve
USERNAME = "test"
PASSWORD = "woeii"
HOST = "localhost"
PORT = 2225
class MySFTPServer(asyncssh.SFTPServer):
def __init__(self, chan):
super().__init__(chan)
self.root = os.path.abspath(SFTP_ROOT)
async def stat(self, path):
"""Handles 'stat' command from SFTP client"""
full_path = os.path.join(self.root, path.lstrip("/"))
return await super().stat(full_path)
async def open(self, path, flags, attrs):
"""Handles file open requests"""
full_path = os.path.join(self.root, path.lstrip("/"))
return await super().open(full_path, flags, attrs)
async def listdir(self, path):
"""Handles directory listing"""
full_path = os.path.join(self.root, path.lstrip("/"))
return await super().listdir(full_path)
class MySSHServer(asyncssh.SSHServer):
"""Custom SSH server to handle authentication"""
def connection_made(self, conn):
print(f"New connection from {conn.get_extra_info('peername')}")
def connection_lost(self, exc):
print("Client disconnected")
def begin_auth(self, username):
return True # No additional authentication steps
def password_auth_supported(self):
return True # Support password authentication
def validate_password(self, username, password):
print(username, password)
return True
return username == USERNAME and password == PASSWORD
async def start_sftp_server():
os.makedirs(SFTP_ROOT, exist_ok=True) # Ensure the root directory exists
await asyncssh.create_server(
lambda: MySSHServer(),
host=HOST,
port=PORT,
server_host_keys=["ssh_host_key"],
process_factory=MySFTPServer,
)
print(f"SFTP server running on {HOST}:{PORT}")
await asyncio.Future() # Keep running forever
if __name__ == "__main__":
try:
asyncio.run(start_sftp_server())
except (OSError, asyncssh.Error) as e:
print(f"Error starting SFTP server: {e}")

View File

@ -1,77 +0,0 @@
import asyncio
import os
import asyncssh
# SSH Server Configuration
HOST = "0.0.0.0"
PORT = 2225
USERNAME = "user"
PASSWORD = "password"
SHELL = "/bin/sh" # Change to another shell if needed
class CustomSSHServer(asyncssh.SSHServer):
def connection_made(self, conn):
print(f"New connection from {conn.get_extra_info('peername')}")
def connection_lost(self, exc):
print("Client disconnected")
def password_auth_supported(self):
return True
def validate_password(self, username, password):
return username == USERNAME and password == PASSWORD
async def custom_bash_process(process):
"""Spawns a custom bash shell process"""
env = os.environ.copy()
env["TERM"] = "xterm-256color"
# Start the Bash shell
bash_proc = await asyncio.create_subprocess_exec(
SHELL,
"-i",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env,
)
async def read_output():
while True:
data = await bash_proc.stdout.read(1)
if not data:
break
process.stdout.write(data)
async def read_input():
while True:
data = await process.stdin.read(1)
if not data:
break
bash_proc.stdin.write(data)
await asyncio.gather(read_output(), read_input())
async def start_ssh_server():
"""Starts the AsyncSSH server with Bash"""
await asyncssh.create_server(
lambda: CustomSSHServer(),
host=HOST,
port=PORT,
server_host_keys=["ssh_host_key"],
process_factory=custom_bash_process,
)
print(f"SSH server running on {HOST}:{PORT}")
await asyncio.Future() # Keep running
if __name__ == "__main__":
try:
asyncio.run(start_ssh_server())
except (OSError, asyncssh.Error) as e:
print(f"Error starting SSH server: {e}")

View File

@ -1,74 +0,0 @@
#!/usr/bin/env python3.7
#
# Copyright (c) 2013-2024 by Ron Frederick <ronf@timeheart.net> and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
# distribution and is available at:
#
# http://www.eclipse.org/legal/epl-2.0/
#
# This program may also be made available under the following secondary
# licenses when the conditions for such availability set forth in the
# Eclipse Public License v2.0 are satisfied:
#
# GNU General Public License, Version 2.0, or any later versions of
# that license
#
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
#
# Contributors:
# Ron Frederick - initial implementation, API, and documentation
# To run this program, the file ``ssh_host_key`` must exist with an SSH
# private key in it to use as a server host key. An SSH host certificate
# can optionally be provided in the file ``ssh_host_key-cert.pub``.
#
# The file ``ssh_user_ca`` must exist with a cert-authority entry of
# the certificate authority which can sign valid client certificates.
import asyncio
import sys
import asyncssh
async def handle_client(process: asyncssh.SSHServerProcess) -> None:
width, height, pixwidth, pixheight = process.term_size
process.stdout.write(
f"Terminal type: {process.term_type}, " f"size: {width}x{height}"
)
if pixwidth and pixheight:
process.stdout.write(f" ({pixwidth}x{pixheight} pixels)")
process.stdout.write("\nTry resizing your window!\n")
while not process.stdin.at_eof():
try:
await process.stdin.read()
except asyncssh.TerminalSizeChanged as exc:
process.stdout.write(f"New window size: {exc.width}x{exc.height}")
if exc.pixwidth and exc.pixheight:
process.stdout.write(f" ({exc.pixwidth}" f"x{exc.pixheight} pixels)")
process.stdout.write("\n")
async def start_server() -> None:
await asyncssh.listen(
"",
2230,
server_host_keys=["ssh_host_key"],
# authorized_client_keys='ssh_user_ca',
process_factory=handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit("Error starting server: " + str(exc))
loop.run_forever()

View File

@ -1,90 +0,0 @@
#!/usr/bin/env python3.7
#
# Copyright (c) 2013-2024 by Ron Frederick <ronf@timeheart.net> and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
# distribution and is available at:
#
# http://www.eclipse.org/legal/epl-2.0/
#
# This program may also be made available under the following secondary
# licenses when the conditions for such availability set forth in the
# Eclipse Public License v2.0 are satisfied:
#
# GNU General Public License, Version 2.0, or any later versions of
# that license
#
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
#
# Contributors:
# Ron Frederick - initial implementation, API, and documentation
# To run this program, the file ``ssh_host_key`` must exist with an SSH
# private key in it to use as a server host key. An SSH host certificate
# can optionally be provided in the file ``ssh_host_key-cert.pub``.
import asyncio
import sys
from typing import Optional
import asyncssh
import bcrypt
passwords = {
"guest": b"", # guest account with no password
"user": bcrypt.hashpw(b"user", bcrypt.gensalt()),
}
def handle_client(process: asyncssh.SSHServerProcess) -> None:
username = process.get_extra_info("username")
process.stdout.write(f"Welcome to my SSH server, {username}!\n")
# process.exit(0)
class MySSHServer(asyncssh.SSHServer):
def connection_made(self, conn: asyncssh.SSHServerConnection) -> None:
peername = conn.get_extra_info("peername")[0]
print(f"SSH connection received from {peername}.")
def connection_lost(self, exc: Optional[Exception]) -> None:
if exc:
print("SSH connection error: " + str(exc), file=sys.stderr)
else:
print("SSH connection closed.")
def begin_auth(self, username: str) -> bool:
# If the user's password is the empty string, no auth is required
return passwords.get(username) != b""
def password_auth_supported(self) -> bool:
return True
def validate_password(self, username: str, password: str) -> bool:
if username not in passwords:
return False
pw = passwords[username]
if not password and not pw:
return True
return bcrypt.checkpw(password.encode("utf-8"), pw)
async def start_server() -> None:
await asyncssh.create_server(
MySSHServer,
"",
2231,
server_host_keys=["ssh_host_key"],
process_factory=handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit("Error starting server: " + str(exc))
loop.run_forever()

View File

@ -1,112 +0,0 @@
#!/usr/bin/env python3.7
#
# Copyright (c) 2016-2024 by Ron Frederick <ronf@timeheart.net> and others.
#
# This program and the accompanying materials are made available under
# the terms of the Eclipse Public License v2.0 which accompanies this
# distribution and is available at:
#
# http://www.eclipse.org/legal/epl-2.0/
#
# This program may also be made available under the following secondary
# licenses when the conditions for such availability set forth in the
# Eclipse Public License v2.0 are satisfied:
#
# GNU General Public License, Version 2.0, or any later versions of
# that license
#
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
#
# Contributors:
# Ron Frederick - initial implementation, API, and documentation
# To run this program, the file ``ssh_host_key`` must exist with an SSH
# private key in it to use as a server host key. An SSH host certificate
# can optionally be provided in the file ``ssh_host_key-cert.pub``.
#
# The file ``ssh_user_ca`` must exist with a cert-authority entry of
# the certificate authority which can sign valid client certificates.
import asyncio
import sys
from typing import List, cast
import asyncssh
class ChatClient:
_clients: List["ChatClient"] = []
def __init__(self, process: asyncssh.SSHServerProcess):
self._process = process
@classmethod
async def handle_client(cls, process: asyncssh.SSHServerProcess):
await cls(process).run()
async def readline(self) -> str:
return cast(str, self._process.stdin.readline())
def write(self, msg: str) -> None:
self._process.stdout.write(msg)
def broadcast(self, msg: str) -> None:
for client in self._clients:
if client != self:
client.write(msg)
def begin_auth(self, username: str) -> bool:
# If the user's password is the empty string, no auth is required
# return False
return True # passwords.get(username) != b''
def password_auth_supported(self) -> bool:
return True
def validate_password(self, username: str, password: str) -> bool:
# if username not in passwords:
# return False
# pw = passwords[username]
# if not password and not pw:
# return True
return True
# return bcrypt.checkpw(password.encode('utf-8'), pw)
async def run(self) -> None:
self.write("Welcome to chat!\n\n")
self.write("Enter your name: ")
name = (await self.readline()).rstrip("\n")
self.write(f"\n{len(self._clients)} other users are connected.\n\n")
self._clients.append(self)
self.broadcast(f"*** {name} has entered chat ***\n")
try:
async for line in self._process.stdin:
self.broadcast(f"{name}: {line}")
except asyncssh.BreakReceived:
pass
self.broadcast(f"*** {name} has left chat ***\n")
self._clients.remove(self)
async def start_server() -> None:
await asyncssh.listen(
"",
2235,
server_host_keys=["ssh_host_key"],
process_factory=ChatClient.handle_client,
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(start_server())
except (OSError, asyncssh.Error) as exc:
sys.exit("Error starting server: " + str(exc))
loop.run_forever()