Author SHA1 Message Date
retoor 5e99e894e9 Not working version, issues with get 2025-08-08 17:28:35 +02:00
retoor 89d639e44e New editor. 2025-08-06 15:33:13 +02:00
retoor e62da0aef1 Bugfix. 2025-08-06 14:18:11 +02:00
retoor 3759306e38 Update sentry. 2025-08-06 11:51:58 +02:00
retoor fcd91b4321 Deleted security. 2025-08-03 03:31:08 +02:00
retoor cf32a78ef5 Removed interval. 2025-08-01 02:06:20 +02:00
retoor 7c43d957bc Randomized skeep. 2025-08-01 01:59:23 +02:00
retoor cc6a9ef9d3 Randomized skeep. 2025-08-01 01:57:18 +02:00
retoor 1babfa0d64 Optional replace.: 2025-07-30 14:49:16 +02:00
retoor ce940b39b8 Optional replace.: 2025-07-30 14:46:10 +02:00
retoor 6151fc1dac Optional replace.: 2025-07-30 14:44:25 +02:00
retoor 338bdb5932 Optional replace.: 2025-07-30 14:41:23 +02:00
retoor bbcc845c26 Optional replace.: 2025-07-30 14:36:56 +02:00
retoor 1c080bc4be Big webdav change. 2025-07-30 03:25:36 +02:00
retoor 59b0494328 Update. 2025-07-30 02:51:26 +02:00
retoor 6337350b60 Update. 2025-07-26 23:56:24 +02:00
retoor 986acfac38 Update. 2025-07-25 17:17:45 +02:00
retoor b27149b5ba Update. 2025-07-25 17:10:03 +02:00
retoor eb1284060a Update attributes. 2025-07-25 17:06:15 +02:00
retoor 4266ac1f12 Less stars 2025-07-25 17:06:15 +02:00
retoor 6b4709d011 Merge pull request 'Add support for once event listeners and improve event removal' (#67) from BordedDev/snek:feat/once-event-listener into main
Reviewed-on: retoor/snek#67
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-07-24 23:36:50 +02:00
retoor ef8d3068a8 Merge pull request 'Fix some message rendering' (#68) from BordedDev/snek:bugfix/message-list-rendering into main
Reviewed-on: retoor/snek#68
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-07-24 23:35:49 +02:00
BordedDev a23c14389b Fix some message rendering 2025-07-24 16:14:51 +02:00
BordedDev 6dfd8db0a6 Add support for once event listeners and improve event removal 2025-07-24 15:59:17 +02:00
retoor abce2e03d1 Fix xss. 2025-07-24 01:46:34 +02:00
retoor 54d7d5b74e Several updates. 2025-07-24 01:07:33 +02:00
retoor 17bb88050a Merge pull request 'Fixed scrolling behavior, reply, cross channel messages and gg navigation' (#66) from BordedDev/snek:bugfix/multiple-issues-with-new-chat into main
Reviewed-on: retoor/snek#66
2025-07-20 19:55:47 +02:00
22 changed files with 2305 additions and 855 deletions
+3 -1
View File
@@ -39,7 +39,9 @@ dependencies = [
"Pillow", "Pillow",
"pillow-heif", "pillow-heif",
"IP2Location", "IP2Location",
"bleach" "bleach",
"sentry-sdk",
"aiosqlite"
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
+8
View File
@@ -9,6 +9,8 @@ from snek.shell import Shell
from snek.app import Application from snek.app import Application
@click.group() @click.group()
def cli(): def cli():
pass pass
@@ -122,6 +124,12 @@ def shell(db_path):
Shell(db_path).run() Shell(db_path).run()
def main(): def main():
try:
import sentry_sdk
sentry_sdk.init("https://ab6147c2f3354c819768c7e89455557b@gt.molodetz.nl/1")
except ImportError:
print("Could not import sentry_sdk")
cli() cli()
+5 -14
View File
@@ -9,7 +9,7 @@ from contextlib import asynccontextmanager
from snek import snode from snek import snode
from snek.view.threads import ThreadsView from snek.view.threads import ThreadsView
from snek.system.ads import AsyncDataSet
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from ipaddress import ip_address from ipaddress import ip_address
@@ -142,6 +142,7 @@ class Application(BaseApplication):
client_max_size=1024 * 1024 * 1024 * 5 * args, client_max_size=1024 * 1024 * 1024 * 5 * args,
**kwargs, **kwargs,
) )
self.db = AsyncDataSet(kwargs["db_path"].replace("sqlite:///", ""))
session_setup(self, EncryptedCookieStorage(SESSION_KEY)) session_setup(self, EncryptedCookieStorage(SESSION_KEY))
self.tasks = asyncio.Queue() self.tasks = asyncio.Queue()
self._middlewares.append(session_middleware) self._middlewares.append(session_middleware)
@@ -174,7 +175,7 @@ class Application(BaseApplication):
self.on_startup.append(self.prepare_asyncio) self.on_startup.append(self.prepare_asyncio)
self.on_startup.append(self.start_user_availability_service) self.on_startup.append(self.start_user_availability_service)
self.on_startup.append(self.start_ssh_server) self.on_startup.append(self.start_ssh_server)
self.on_startup.append(self.prepare_database) #self.on_startup.append(self.prepare_database)
async def prepare_stats(self, app): async def prepare_stats(self, app):
app['stats'] = create_stats_structure() app['stats'] = create_stats_structure()
@@ -245,18 +246,8 @@ class Application(BaseApplication):
async def prepare_database(self, app): async def prepare_database(self, app):
self.db.query("PRAGMA journal_mode=WAL") await self.db.query_raw("PRAGMA journal_mode=WAL")
self.db.query("PRAGMA syncnorm=off") await self.db.query_raw("PRAGMA syncnorm=off")
try:
if not self.db["user"].has_index("username"):
self.db["user"].create_index("username", unique=True)
if not self.db["channel_member"].has_index(["channel_uid", "user_uid"]):
self.db["channel_member"].create_index(["channel_uid", "user_uid"])
if not self.db["channel_message"].has_index(["channel_uid", "user_uid"]):
self.db["channel_message"].create_index(["channel_uid", "user_uid"])
except:
pass
await self.services.drive.prepare_all() await self.services.drive.prepare_all()
self.loop.create_task(self.task_runner()) self.loop.create_task(self.task_runner())
+4
View File
@@ -13,6 +13,10 @@ class ChannelModel(BaseModel):
last_message_on = ModelField(name="last_message_on", required=False, kind=str) last_message_on = ModelField(name="last_message_on", required=False, kind=str)
history_start = ModelField(name="history_start", required=False, kind=str) history_start = ModelField(name="history_start", required=False, kind=str)
@property
def is_dm(self):
return 'dm' in self['tag'].lower()
async def get_last_message(self) -> ChannelMessageModel: async def get_last_message(self) -> ChannelMessageModel:
history_start_filter = "" history_start_filter = ""
if self["history_start"]: if self["history_start"]:
+3 -3
View File
@@ -1,5 +1,5 @@
from snek.system.service import BaseService from snek.system.service import BaseService
from snek.system.template import whitelist_attributes from snek.system.template import sanitize_html
import time import time
class ChannelMessageService(BaseService): class ChannelMessageService(BaseService):
@@ -72,7 +72,7 @@ class ChannelMessageService(BaseService):
try: try:
template = self.app.jinja2_env.get_template("message.html") template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context) model["html"] = template.render(**context)
model["html"] = whitelist_attributes(model["html"]) model['html'] = sanitize_html(model['html'])
except Exception as ex: except Exception as ex:
print(ex, flush=True) print(ex, flush=True)
@@ -129,7 +129,7 @@ class ChannelMessageService(BaseService):
) )
template = self.app.jinja2_env.get_template("message.html") template = self.app.jinja2_env.get_template("message.html")
model["html"] = template.render(**context) model["html"] = template.render(**context)
model["html"] = whitelist_attributes(model["html"]) model['html'] = sanitize_html(model['html'])
return await super().save(model) return await super().save(model)
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30): async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
+1 -1
View File
@@ -77,7 +77,7 @@ class SocketService(BaseService):
async def send_to_user(self, user_uid, message): async def send_to_user(self, user_uid, message):
count = 0 count = 0
for s in self.users.get(user_uid, []): for s in list(self.users.get(user_uid, [])):
if await s.send_json(message): if await s.send_json(message):
count += 1 count += 1
return count return count
+2
View File
@@ -101,6 +101,8 @@ class UserService(BaseService):
model.username.value = username model.username.value = username
model.password.value = await security.hash(password) model.password.value = await security.hash(password)
if await self.save(model): if await self.save(model):
for x in range(10):
print("Jazeker!!!")
if model: if model:
channel = await self.services.channel.ensure_public_channel( channel = await self.services.channel.ensure_public_channel(
model["uid"] model["uid"]
+2 -1
View File
@@ -7,7 +7,8 @@ class UserPropertyService(BaseService):
mapper_name = "user_property" mapper_name = "user_property"
async def set(self, user_uid, name, value): async def set(self, user_uid, name, value):
self.mapper.db["user_property"].upsert( self.mapper.db.upsert(
"user_property",
{ {
"user_uid": user_uid, "user_uid": user_uid,
"name": name, "name": name,
+348 -80
View File
@@ -7,32 +7,92 @@ import { NjetComponent} from "/njet.js"
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
:host {
display: block;
position: relative;
height: 100%;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
#editor { #editor {
padding: 1rem; padding: 1rem;
outline: none; outline: none;
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5; line-height: 1.5;
height: 100%; height: calc(100% - 30px);
overflow-y: auto; overflow-y: auto;
background: #1e1e1e; background: #1e1e1e;
color: #d4d4d4; color: #d4d4d4;
font-size: 14px;
caret-color: #fff;
} }
#command-line {
#editor.insert-mode {
caret-color: #4ec9b0;
}
#editor.visual-mode {
caret-color: #c586c0;
}
#editor::selection {
background: #264f78;
}
#status-bar {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0;
height: 30px;
background: #007acc;
color: #fff;
display: flex;
align-items: center;
padding: 0 1rem;
font-size: 12px;
font-weight: 500;
}
#mode-indicator {
text-transform: uppercase;
margin-right: 20px;
font-weight: bold;
}
#command-line {
position: absolute;
bottom: 30px;
left: 0;
width: 100%; width: 100%;
padding: 0.2rem 1rem; padding: 0.3rem 1rem;
background: #333; background: #2d2d2d;
color: #0f0; color: #d4d4d4;
display: none; display: none;
font-family: monospace; font-family: inherit;
border-top: 1px solid #3e3e3e;
font-size: 14px;
}
#command-input {
background: transparent;
border: none;
color: inherit;
outline: none;
font-family: inherit;
font-size: inherit;
width: calc(100% - 20px);
}
.visual-selection {
background: #264f78 !important;
} }
`; `;
this.editor = document.createElement('div'); this.editor = document.createElement('div');
this.editor.id = 'editor'; this.editor.id = 'editor';
this.editor.contentEditable = true; this.editor.contentEditable = true;
this.editor.spellcheck = false;
this.editor.innerText = `Welcome to VimEditor Component this.editor.innerText = `Welcome to VimEditor Component
Line 2 here Line 2 here
Another line Another line
@@ -40,22 +100,90 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
this.cmdLine = document.createElement('div'); this.cmdLine = document.createElement('div');
this.cmdLine.id = 'command-line'; this.cmdLine.id = 'command-line';
this.shadowRoot.append(style, this.editor, this.cmdLine);
this.mode = 'normal'; // normal | insert | visual | command const cmdPrompt = document.createElement('span');
cmdPrompt.textContent = ':';
this.cmdInput = document.createElement('input');
this.cmdInput.id = 'command-input';
this.cmdInput.type = 'text';
this.cmdLine.append(cmdPrompt, this.cmdInput);
this.statusBar = document.createElement('div');
this.statusBar.id = 'status-bar';
this.modeIndicator = document.createElement('span');
this.modeIndicator.id = 'mode-indicator';
this.modeIndicator.textContent = 'NORMAL';
this.statusBar.appendChild(this.modeIndicator);
this.shadowRoot.append(style, this.editor, this.cmdLine, this.statusBar);
this.mode = 'normal';
this.keyBuffer = ''; this.keyBuffer = '';
this.lastDeletedLine = ''; this.lastDeletedLine = '';
this.yankedLine = ''; this.yankedLine = '';
this.visualStartOffset = null;
this.visualEndOffset = null;
this.editor.addEventListener('keydown', this.handleKeydown.bind(this)); // Bind event handlers
this.handleKeydown = this.handleKeydown.bind(this);
this.handleCmdKeydown = this.handleCmdKeydown.bind(this);
this.updateVisualSelection = this.updateVisualSelection.bind(this);
this.editor.addEventListener('keydown', this.handleKeydown);
this.cmdInput.addEventListener('keydown', this.handleCmdKeydown);
this.editor.addEventListener('beforeinput', this.handleBeforeInput.bind(this));
} }
connectedCallback() { connectedCallback() {
this.editor.focus(); this.editor.focus();
} }
setMode(mode) {
this.mode = mode;
this.modeIndicator.textContent = mode.toUpperCase();
// Update editor classes
this.editor.classList.remove('insert-mode', 'visual-mode', 'normal-mode');
this.editor.classList.add(`${mode}-mode`);
if (mode === 'visual') {
this.visualStartOffset = this.getCaretOffset();
this.editor.addEventListener('selectionchange', this.updateVisualSelection);
} else {
this.clearVisualSelection();
this.editor.removeEventListener('selectionchange', this.updateVisualSelection);
}
if (mode === 'command') {
this.cmdLine.style.display = 'block';
this.cmdInput.value = '';
this.cmdInput.focus();
} else {
this.cmdLine.style.display = 'none';
if (mode !== 'insert') {
// Keep focus on editor for all non-insert modes
this.editor.focus();
}
}
}
updateVisualSelection() {
if (this.mode !== 'visual') return;
this.visualEndOffset = this.getCaretOffset();
}
clearVisualSelection() {
const sel = this.shadowRoot.getSelection();
if (sel) sel.removeAllRanges();
this.visualStartOffset = null;
this.visualEndOffset = null;
}
getCaretOffset() { getCaretOffset() {
let caretOffset = 0;
const sel = this.shadowRoot.getSelection(); const sel = this.shadowRoot.getSelection();
if (!sel || sel.rangeCount === 0) return 0; if (!sel || sel.rangeCount === 0) return 0;
@@ -63,160 +191,300 @@ Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
const preCaretRange = range.cloneRange(); const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor); preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset); preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length; return preCaretRange.toString().length;
return caretOffset;
} }
setCaretOffset(offset) { setCaretOffset(offset) {
const textContent = this.editor.innerText;
offset = Math.max(0, Math.min(offset, textContent.length));
const range = document.createRange(); const range = document.createRange();
const sel = this.shadowRoot.getSelection(); const sel = this.shadowRoot.getSelection();
const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false); const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentOffset = 0; let currentOffset = 0;
let node; let node;
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
if (currentOffset + node.length >= offset) { const nodeLength = node.textContent.length;
if (currentOffset + nodeLength >= offset) {
range.setStart(node, offset - currentOffset); range.setStart(node, offset - currentOffset);
range.collapse(true); range.collapse(true);
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
return; return;
} }
currentOffset += node.length; currentOffset += nodeLength;
} }
// If we couldn't find the position, set to end
if (this.editor.lastChild) {
range.selectNodeContents(this.editor.lastChild);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
}
handleBeforeInput(e) {
if (this.mode !== 'insert') {
e.preventDefault();
}
}
handleCmdKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.executeCommand(this.cmdInput.value);
this.setMode('normal');
} else if (e.key === 'Escape') {
e.preventDefault();
this.setMode('normal');
}
}
executeCommand(cmd) {
const trimmedCmd = cmd.trim();
// Handle basic vim commands
if (trimmedCmd === 'w' || trimmedCmd === 'write') {
console.log('Save command (not implemented)');
} else if (trimmedCmd === 'q' || trimmedCmd === 'quit') {
console.log('Quit command (not implemented)');
} else if (trimmedCmd === 'wq' || trimmedCmd === 'x') {
console.log('Save and quit command (not implemented)');
} else if (/^\d+$/.test(trimmedCmd)) {
// Go to line number
const lineNum = parseInt(trimmedCmd, 10) - 1;
this.goToLine(lineNum);
}
}
goToLine(lineNum) {
const lines = this.editor.innerText.split('\n');
if (lineNum < 0 || lineNum >= lines.length) return;
let offset = 0;
for (let i = 0; i < lineNum; i++) {
offset += lines[i].length + 1;
}
this.setCaretOffset(offset);
}
getCurrentLineInfo() {
const text = this.editor.innerText;
const caretPos = this.getCaretOffset();
const lines = text.split('\n');
let charCount = 0;
for (let i = 0; i < lines.length; i++) {
if (caretPos <= charCount + lines[i].length) {
return {
lineIndex: i,
lines: lines,
lineStartOffset: charCount,
positionInLine: caretPos - charCount
};
}
charCount += lines[i].length + 1;
}
return {
lineIndex: lines.length - 1,
lines: lines,
lineStartOffset: charCount - lines[lines.length - 1].length - 1,
positionInLine: 0
};
} }
handleKeydown(e) { handleKeydown(e) {
const key = e.key;
if (this.mode === 'insert') { if (this.mode === 'insert') {
if (key === 'Escape') { if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
this.mode = 'normal'; this.setMode('normal');
this.editor.blur(); // Move cursor one position left (vim behavior)
this.editor.focus(); const offset = this.getCaretOffset();
if (offset > 0) {
this.setCaretOffset(offset - 1);
}
} }
return; return;
} }
if (this.mode === 'command') { if (this.mode === 'command') {
if (key === 'Enter' || key === 'Escape') { return; // Command mode input is handled by cmdInput
e.preventDefault();
this.cmdLine.style.display = 'none';
this.mode = 'normal';
this.keyBuffer = '';
}
return;
} }
if (this.mode === 'visual') { if (this.mode === 'visual') {
if (key === 'Escape') { if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
this.mode = 'normal'; this.setMode('normal');
}
return; return;
} }
// Handle normal mode // Allow movement in visual mode
this.keyBuffer += key; if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
return; // Let default behavior handle selection
const text = this.editor.innerText;
const caretPos = this.getCaretOffset();
const lines = text.split('\n');
let charCount = 0, lineIdx = 0;
for (let i = 0; i < lines.length; i++) {
if (caretPos <= charCount + lines[i].length) {
lineIdx = i;
break;
}
charCount += lines[i].length + 1;
} }
const offsetToLine = idx => if (e.key === 'y') {
text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0); e.preventDefault();
// Yank selected text
const sel = this.shadowRoot.getSelection();
if (sel && sel.rangeCount > 0) {
this.yankedLine = sel.toString();
}
this.setMode('normal');
return;
}
if (e.key === 'd' || e.key === 'x') {
e.preventDefault();
// Delete selected text
const sel = this.shadowRoot.getSelection();
if (sel && sel.rangeCount > 0) {
this.lastDeletedLine = sel.toString();
document.execCommand('delete');
}
this.setMode('normal');
return;
}
}
// Normal mode handling
e.preventDefault();
// Special keys that should be handled immediately
if (e.key === 'Escape') {
this.keyBuffer = '';
this.setMode('normal');
return;
}
// Build key buffer for commands
this.keyBuffer += e.key;
const lineInfo = this.getCurrentLineInfo();
const { lineIndex, lines, lineStartOffset, positionInLine } = lineInfo;
// Process commands
switch (this.keyBuffer) { switch (this.keyBuffer) {
case 'i': case 'i':
e.preventDefault();
this.mode = 'insert';
this.keyBuffer = ''; this.keyBuffer = '';
this.setMode('insert');
break;
case 'a':
this.keyBuffer = '';
this.setCaretOffset(this.getCaretOffset() + 1);
this.setMode('insert');
break; break;
case 'v': case 'v':
e.preventDefault();
this.mode = 'visual';
this.keyBuffer = ''; this.keyBuffer = '';
this.setMode('visual');
break; break;
case ':': case ':':
e.preventDefault();
this.mode = 'command';
this.cmdLine.style.display = 'block';
this.cmdLine.textContent = ':';
this.keyBuffer = ''; this.keyBuffer = '';
this.setMode('command');
break; break;
case 'yy': case 'yy':
e.preventDefault();
this.yankedLine = lines[lineIdx];
this.keyBuffer = ''; this.keyBuffer = '';
this.yankedLine = lines[lineIndex];
break; break;
case 'dd': case 'dd':
e.preventDefault();
this.lastDeletedLine = lines[lineIdx];
lines.splice(lineIdx, 1);
this.editor.innerText = lines.join('\n');
this.setCaretOffset(offsetToLine(lineIdx));
this.keyBuffer = ''; this.keyBuffer = '';
this.lastDeletedLine = lines[lineIndex];
lines.splice(lineIndex, 1);
if (lines.length === 0) lines.push('');
this.editor.innerText = lines.join('\n');
this.setCaretOffset(lineStartOffset);
break; break;
case 'p': case 'p':
e.preventDefault(); this.keyBuffer = '';
const lineToPaste = this.yankedLine || this.lastDeletedLine; const lineToPaste = this.yankedLine || this.lastDeletedLine;
if (lineToPaste) { if (lineToPaste) {
lines.splice(lineIdx + 1, 0, lineToPaste); lines.splice(lineIndex + 1, 0, lineToPaste);
this.editor.innerText = lines.join('\n'); this.editor.innerText = lines.join('\n');
this.setCaretOffset(offsetToLine(lineIdx + 1)); this.setCaretOffset(lineStartOffset + lines[lineIndex].length + 1);
} }
this.keyBuffer = '';
break; break;
case '0': case '0':
e.preventDefault();
this.setCaretOffset(offsetToLine(lineIdx));
this.keyBuffer = ''; this.keyBuffer = '';
this.setCaretOffset(lineStartOffset);
break; break;
case '$': case '$':
e.preventDefault();
this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length);
this.keyBuffer = ''; this.keyBuffer = '';
this.setCaretOffset(lineStartOffset + lines[lineIndex].length);
break; break;
case 'gg': case 'gg':
e.preventDefault();
this.setCaretOffset(0);
this.keyBuffer = ''; this.keyBuffer = '';
this.setCaretOffset(0);
break; break;
case 'G': case 'G':
e.preventDefault();
this.setCaretOffset(text.length);
this.keyBuffer = ''; this.keyBuffer = '';
this.setCaretOffset(this.editor.innerText.length);
break; break;
case 'Escape': case 'h':
e.preventDefault(); case 'ArrowLeft':
this.mode = 'normal';
this.keyBuffer = ''; this.keyBuffer = '';
this.cmdLine.style.display = 'none'; const currentOffset = this.getCaretOffset();
if (currentOffset > 0) {
this.setCaretOffset(currentOffset - 1);
}
break;
case 'l':
case 'ArrowRight':
this.keyBuffer = '';
this.setCaretOffset(this.getCaretOffset() + 1);
break;
case 'j':
case 'ArrowDown':
this.keyBuffer = '';
if (lineIndex < lines.length - 1) {
const nextLineStart = lineStartOffset + lines[lineIndex].length + 1;
const nextLineLength = lines[lineIndex + 1].length;
const newPosition = Math.min(positionInLine, nextLineLength);
this.setCaretOffset(nextLineStart + newPosition);
}
break;
case 'k':
case 'ArrowUp':
this.keyBuffer = '';
if (lineIndex > 0) {
let prevLineStart = 0;
for (let i = 0; i < lineIndex - 1; i++) {
prevLineStart += lines[i].length + 1;
}
const prevLineLength = lines[lineIndex - 1].length;
const newPosition = Math.min(positionInLine, prevLineLength);
this.setCaretOffset(prevLineStart + newPosition);
}
break; break;
default: default:
// allow up to 2 chars for combos // Clear buffer if it gets too long or contains invalid sequences
if (this.keyBuffer.length > 2) this.keyBuffer = ''; if (this.keyBuffer.length > 2 ||
(this.keyBuffer.length === 2 && !['dd', 'yy', 'gg'].includes(this.keyBuffer))) {
this.keyBuffer = '';
}
break; break;
} }
} }
+19 -1
View File
@@ -3,8 +3,15 @@ export class EventHandler {
this.subscribers = {}; this.subscribers = {};
} }
addEventListener(type, handler) { addEventListener(type, handler, { once = false } = {}) {
if (!this.subscribers[type]) this.subscribers[type] = []; if (!this.subscribers[type]) this.subscribers[type] = [];
if (once) {
const originalHandler = handler;
handler = (...args) => {
originalHandler(...args);
this.removeEventListener(type, handler);
};
}
this.subscribers[type].push(handler); this.subscribers[type].push(handler);
} }
@@ -12,4 +19,15 @@ export class EventHandler {
if (this.subscribers[type]) if (this.subscribers[type])
this.subscribers[type].forEach((handler) => handler(...data)); this.subscribers[type].forEach((handler) => handler(...data));
} }
removeEventListener(type, handler) {
if (!this.subscribers[type]) return;
this.subscribers[type] = this.subscribers[type].filter(
(h) => h !== handler
);
if (this.subscribers[type].length === 0) {
delete this.subscribers[type];
}
}
} }
+84 -85
View File
@@ -7,38 +7,40 @@
// MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty. // MIT License: This is free software. Permission is granted to use, copy, modify, and/or distribute this software for any purpose with or without fee. The software is provided "as is" without any warranty.
import { app } from "./app.js"; import { app } from "./app.js";
const LONG_TIME = 1000 * 60 * 20 const LONG_TIME = 1000 * 60 * 20;
export class ReplyEvent extends Event { export class ReplyEvent extends Event {
constructor(messageTextTarget) { constructor(messageTextTarget) {
super('reply', { bubbles: true, composed: true }); super('reply', { bubbles: true, composed: true });
this.messageTextTarget = messageTextTarget; this.messageTextTarget = messageTextTarget;
// Clone and sanitize message node to text-only reply
const newMessage = messageTextTarget.cloneNode(true); const newMessage = messageTextTarget.cloneNode(true);
newMessage.style.maxHeight = "0" newMessage.style.maxHeight = "0";
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget); messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
newMessage.querySelectorAll('.embed-url-link').forEach(link => { // Remove all .embed-url-link
link.remove() newMessage.querySelectorAll('.embed-url-link').forEach(link => link.remove());
})
// Replace <picture> with their <img>
newMessage.querySelectorAll('picture').forEach(picture => { newMessage.querySelectorAll('picture').forEach(picture => {
const img = picture.querySelector('img'); const img = picture.querySelector('img');
if (img) { if (img) picture.replaceWith(img);
picture.replaceWith(img); });
}
})
// Replace <img> with just their src
newMessage.querySelectorAll('img').forEach(img => { newMessage.querySelectorAll('img').forEach(img => {
const src = img.src || img.currentSrc; const src = img.src || img.currentSrc;
img.replaceWith(document.createTextNode(src)); img.replaceWith(document.createTextNode(src));
}) });
// Replace <iframe> with their src
newMessage.querySelectorAll('iframe').forEach(iframe => { newMessage.querySelectorAll('iframe').forEach(iframe => {
const src = iframe.src || iframe.currentSrc; const src = iframe.src || iframe.currentSrc;
iframe.replaceWith(document.createTextNode(src)); iframe.replaceWith(document.createTextNode(src));
}) });
// Replace <a> with href or markdown
newMessage.querySelectorAll('a').forEach(a => { newMessage.querySelectorAll('a').forEach(a => {
const href = a.getAttribute('href'); const href = a.getAttribute('href');
const text = a.innerText || a.textContent; const text = a.innerText || a.textContent;
@@ -47,33 +49,21 @@ export class ReplyEvent extends Event {
} else { } else {
a.replaceWith(document.createTextNode(`[${text}](${href})`)); a.replaceWith(document.createTextNode(`[${text}](${href})`));
} }
}) });
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim(); this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
newMessage.remove() newMessage.remove();
} }
} }
class MessageElement extends HTMLElement { class MessageElement extends HTMLElement {
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
isVisible() {
if (!this) return false;
const rect = this.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
updateUI() { updateUI() {
if (this._originalChildren === undefined) { if (this._originalChildren === undefined) {
const { color, user_nick, created_at, user_uid } = this.dataset; const { color, user_nick, created_at, user_uid } = this.dataset;
this.classList.add('message'); this.classList.add('message');
this.style.maxWidth = '100%'; this.style.maxWidth = '100%';
this._originalChildren = Array.from(this.children); this._originalChildren = Array.from(this.children);
this.innerHTML = ` this.innerHTML = `
<a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html"> <a class="avatar" style="background-color: ${color || ''}; color: black;" href="/user/${user_uid || ''}.html">
<img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy"> <img class="avatar-img" width="40" height="40" src="/avatar/${user_uid || ''}.svg" alt="${user_nick || ''}" loading="lazy">
@@ -83,12 +73,12 @@ class MessageElement extends HTMLElement {
<div class="text"></div> <div class="text"></div>
<div class="time no-select" data-created_at="${created_at || ''}"> <div class="time no-select" data-created_at="${created_at || ''}">
<span></span> <span></span>
<a href="#reply">reply</a></div> <a href="#reply">reply</a>
</div>
</div> </div>
`; `;
this.messageDiv = this.querySelector('.text'); this.messageDiv = this.querySelector('.text');
if (this._originalChildren && this._originalChildren.length > 0) { if (this._originalChildren && this._originalChildren.length > 0) {
this._originalChildren.forEach(child => { this._originalChildren.forEach(child => {
this.messageDiv.appendChild(child); this.messageDiv.appendChild(child);
@@ -101,11 +91,12 @@ class MessageElement extends HTMLElement {
this.replyDiv.addEventListener('click', (e) => { this.replyDiv.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
this.dispatchEvent(new ReplyEvent(this.messageDiv)); this.dispatchEvent(new ReplyEvent(this.messageDiv));
}) });
} }
if (!this.siblingGenerated && this.nextElementSibling) { // Sibling logic for user switches and long time gaps
this.siblingGenerated = true; if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
this.siblingGenerated = this.nextElementSibling;
if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) { if (this.nextElementSibling?.dataset?.user_uid !== this.dataset.user_uid) {
this.classList.add('switch-user'); this.classList.add('switch-user');
} else { } else {
@@ -126,7 +117,7 @@ class MessageElement extends HTMLElement {
updateMessage(...messages) { updateMessage(...messages) {
if (this._originalChildren) { if (this._originalChildren) {
this.messageDiv.replaceChildren(...messages) this.messageDiv.replaceChildren(...messages);
this._originalChildren = messages; this._originalChildren = messages;
} }
} }
@@ -135,20 +126,50 @@ class MessageElement extends HTMLElement {
this.updateUI(); this.updateUI();
} }
disconnectedCallback() { disconnectedCallback() {}
} connectedMoveCallback() {}
connectedMoveCallback() {
}
attributeChangedCallback(name, oldValue, newValue) { attributeChangedCallback(name, oldValue, newValue) {
this.updateUI() this.updateUI();
} }
} }
class MessageList extends HTMLElement { class MessageList extends HTMLElement {
constructor() { constructor() {
super(); super();
this.messageMap = new Map();
this.visibleSet = new Set();
this._observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.visibleSet.add(entry.target);
if (entry.target instanceof MessageElement) {
entry.target.updateUI();
}
} else {
this.visibleSet.delete(entry.target);
}
});
}, {
root: this,
threshold: 0,
});
// End-of-messages marker
this.endOfMessages = document.createElement('div');
this.endOfMessages.classList.add('message-list-bottom');
this.prepend(this.endOfMessages);
// Observe existing children and index by uid
for (const c of this.children) {
this._observer.observe(c);
if (c instanceof MessageElement) {
this.messageMap.set(c.dataset.uid, c);
}
}
// Wire up socket events
app.ws.addEventListener("update_message_text", (data) => { app.ws.addEventListener("update_message_text", (data) => {
if (this.messageMap.has(data.uid)) { if (this.messageMap.has(data.uid)) {
this.upsertMessage(data); this.upsertMessage(data);
@@ -158,45 +179,17 @@ class MessageList extends HTMLElement {
this.triggerGlow(data.user_uid, data.color); this.triggerGlow(data.user_uid, data.color);
}); });
this.messageMap = new Map();
this.visibleSet = new Set();
this._observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.visibleSet.add(entry.target);
const messageElement = entry.target;
if (messageElement instanceof MessageElement) {
messageElement.updateUI();
}
} else {
this.visibleSet.delete(entry.target);
}
});
}, {
root: this,
threshold: 0,
})
for(const c of this.children) {
this._observer.observe(c);
if (c instanceof MessageElement) {
this.messageMap.set(c.dataset.uid, c);
}
}
this.endOfMessages = document.createElement('div');
this.endOfMessages.classList.add('message-list-bottom');
this.prepend(this.endOfMessages);
this.scrollToBottom(true); this.scrollToBottom(true);
} }
connectedCallback() { connectedCallback() {
this.addEventListener('click', (e) => { this.addEventListener('click', (e) => {
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return; if (
e.target.tagName !== 'IMG' ||
e.target.classList.contains('avatar-img')
) return;
const img = e.target; const img = e.target;
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;cursor:pointer;';
@@ -217,12 +210,11 @@ class MessageList extends HTMLElement {
overlay.appendChild(fullImg); overlay.appendChild(fullImg);
document.body.appendChild(overlay); document.body.appendChild(overlay);
overlay.addEventListener('click', () => { overlay.addEventListener('click', () => {
if (overlay.parentNode) { if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
overlay.parentNode.removeChild(overlay);
}
}); });
// Optional: ESC key closes overlay // ESC to close
const escListener = (evt) => { const escListener = (evt) => {
if (evt.key === 'Escape') { if (evt.key === 'Escape') {
if (overlay.parentNode) overlay.parentNode.removeChild(overlay); if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
@@ -230,8 +222,9 @@ class MessageList extends HTMLElement {
} }
}; };
document.addEventListener('keydown', escListener); document.addEventListener('keydown', escListener);
}) });
} }
isElementVisible(element) { isElementVisible(element) {
if (!element) return false; if (!element) return false;
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
@@ -242,14 +235,16 @@ class MessageList extends HTMLElement {
rect.right <= (window.innerWidth || document.documentElement.clientWidth) rect.right <= (window.innerWidth || document.documentElement.clientWidth)
); );
} }
isScrolledToBottom() { isScrolledToBottom() {
return this.isElementVisible(this.endOfMessages); return this.visibleSet.has(this.endOfMessages);
} }
scrollToBottom(force = false, behavior = 'instant') { scrollToBottom(force = false, behavior = 'instant') {
if (force || !this.isScrolledToBottom()) { if (force || !this.isScrolledToBottom()) {
this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
setTimeout(() => { setTimeout(() => {
this.firstElementChild.scrollIntoView({ behavior, block: 'end' }); this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
}, 200); }, 200);
} }
} }
@@ -272,40 +267,44 @@ class MessageList extends HTMLElement {
} }
} }
updateTimes() { updateTimes() {
this.visibleSet.forEach((messageElement) => { this.visibleSet.forEach((messageElement) => {
if (messageElement instanceof MessageElement) { if (messageElement instanceof MessageElement) {
messageElement.updateUI(); messageElement.updateUI();
} }
}) });
} }
upsertMessage(data) { upsertMessage(data) {
let message = this.messageMap.get(data.uid); let message = this.messageMap.get(data.uid);
if (message) { if (message && (data.is_final || !data.message)) {
message.parentElement?.removeChild(message); message.parentElement?.removeChild(message);
} // TO force insert
message = null;
if (!data.message) return }
if (!data.message) return;
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.innerHTML = data.html; wrapper.innerHTML = data.html;
if (message) { if (message) {
// If the old element is already custom, only update its message children
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children)); message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
} else { } else {
// If not, insert the new one and observe
message = wrapper.firstElementChild; message = wrapper.firstElementChild;
this.messageMap.set(data.uid, message); this.messageMap.set(data.uid, message);
this._observer.observe(message); this._observer.observe(message);
this.endOfMessages.after(message);
} }
const scrolledToBottom = this.isScrolledToBottom(); const scrolledToBottom = this.isScrolledToBottom();
this.prepend(message);
if (scrolledToBottom) this.scrollToBottom(true); if (scrolledToBottom) this.scrollToBottom(true);
} }
} }
customElements.define("chat-message", MessageElement); customElements.define("chat-message", MessageElement);
customElements.define("message-list", MessageList); customElements.define("message-list", MessageList);
+37 -11
View File
@@ -1,5 +1,3 @@
class RestClient { class RestClient {
constructor({ baseURL = '', headers = {} } = {}) { constructor({ baseURL = '', headers = {} } = {}) {
this.baseURL = baseURL; this.baseURL = baseURL;
@@ -210,27 +208,52 @@ class Njet extends HTMLElement {
customElements.define(name, component); customElements.define(name, component);
} }
constructor() { constructor(config) {
super(); super();
// Store the config for use in render and other methods
this.config = config || {};
if (!Njet._root) { if (!Njet._root) {
Njet._root = this Njet._root = this
Njet._rest = new RestClient({ baseURL: '/' || null }) Njet._rest = new RestClient({ baseURL: '/' || null })
} }
this.root._elements.push(this) this.root._elements.push(this)
this.classList.add('njet'); this.classList.add('njet');
// Initialize properties from config before rendering
this.initProps(this.config);
// Call render after properties are initialized
this.render.call(this); this.render.call(this);
//this.initProps(config);
//if (typeof this.config.construct === 'function') // Call construct if defined
// this.config.construct.call(this) if (typeof this.config.construct === 'function') {
this.config.construct.call(this)
}
} }
initProps(config) { initProps(config) {
const props = Object.keys(config) const props = Object.keys(config)
props.forEach(prop => { props.forEach(prop => {
if (config[prop] !== undefined) { // Skip special properties that are handled separately
if (['construct', 'items', 'classes'].includes(prop)) {
return;
}
// Check if there's a setter for this property
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), prop);
if (descriptor && descriptor.set) {
// Use the setter
this[prop] = config[prop]; this[prop] = config[prop];
} else if (prop in this) {
// Property exists, set it directly
this[prop] = config[prop];
} else {
// Set as attribute for unknown properties
this.setAttribute(prop, config[prop]);
} }
}); });
if (config.classes) { if (config.classes) {
this.classList.add(...config.classes); this.classList.add(...config.classes);
} }
@@ -342,7 +365,7 @@ class NjetDialog extends Component {
const buttonContainer = document.createElement('div'); const buttonContainer = document.createElement('div');
buttonContainer.style.marginTop = '20px'; buttonContainer.style.marginTop = '20px';
buttonContainer.style.display = 'flex'; buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flenjet-end'; buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '10px'; buttonContainer.style.gap = '10px';
if (secondaryButton) { if (secondaryButton) {
const secondary = new NjetButton(secondaryButton); const secondary = new NjetButton(secondaryButton);
@@ -372,8 +395,9 @@ class NjetWindow extends Component {
header.textContent = title; header.textContent = title;
this.appendChild(header); this.appendChild(header);
} }
if (this.config.items) {
this.config.items.forEach(item => this.appendChild(item)); this.config.items.forEach(item => this.appendChild(item));
}
} }
show(){ show(){
@@ -408,7 +432,8 @@ class NjetGrid extends Component {
} }
} }
Njet.registerComponent('njet-grid', NjetGrid); Njet.registerComponent('njet-grid', NjetGrid);
/*
/* Example usage:
const button = new NjetButton({ const button = new NjetButton({
classes: ['my-button'], classes: ['my-button'],
text: 'Shared', text: 'Shared',
@@ -545,11 +570,12 @@ njet.showWindow = function(args) {
return w return w
} }
njet.publish = function(event, data) { njet.publish = function(event, data) {
if (this.root._subscriptions[event]) { if (this.root && this.root._subscriptions && this.root._subscriptions[event]) {
this.root._subscriptions[event].forEach(callback => callback(data)) this.root._subscriptions[event].forEach(callback => callback(data))
} }
} }
njet.subscribe = function(event, callback) { njet.subscribe = function(event, callback) {
if (!this.root) return;
if (!this.root._subscriptions[event]) { if (!this.root._subscriptions[event]) {
this.root._subscriptions[event] = [] this.root._subscriptions[event] = []
} }
+2 -3
View File
@@ -142,10 +142,9 @@ export class Socket extends EventHandler {
method, method,
args, args,
}; };
const me = this;
return new Promise((resolve) => { return new Promise((resolve) => {
me.addEventListener(call.callId, (data) => resolve(data)); this.addEventListener(call.callId, (data) => resolve(data), { once: true});
me.sendJson(call); this.sendJson(call);
}); });
} }
} }
File diff suppressed because it is too large Load Diff
+19 -11
View File
@@ -1,7 +1,7 @@
DEFAULT_LIMIT = 30 DEFAULT_LIMIT = 30
import asyncio import asyncio
import typing import typing
import traceback
from snek.system.model import BaseModel from snek.system.model import BaseModel
@@ -51,7 +51,9 @@ class BaseMapper:
kwargs["uid"] = uid kwargs["uid"] = uid
if not kwargs.get("deleted_at"): if not kwargs.get("deleted_at"):
kwargs["deleted_at"] = None kwargs["deleted_at"] = None
record = await self.run_in_executor(self.table.find_one, **kwargs) #traceback.print_exc()
record = await self.db.get(self.table_name, kwargs)
if not record: if not record:
return None return None
record = dict(record) record = dict(record)
@@ -61,23 +63,29 @@ class BaseMapper:
return model return model
async def exists(self, **kwargs): async def exists(self, **kwargs):
return await self.run_in_executor(self.table.exists, **kwargs) return await self.db.count(self.table_name, kwargs)
#return await self.run_in_executor(self.table.exists, **kwargs)
async def count(self, **kwargs) -> int: async def count(self, **kwargs) -> int:
return await self.run_in_executor(self.table.count, **kwargs) return await self.db.count(self.table_name,kwargs)
async def save(self, model: BaseModel) -> bool: async def save(self, model: BaseModel) -> bool:
if not model.record.get("uid"): if not model.record.get("uid"):
raise Exception(f"Attempt to save without uid: {model.record}.") raise Exception(f"Attempt to save without uid: {model.record}.")
model.updated_at.update() model.updated_at.update()
return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) await self.upsert(model)
return model
#return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True)
async def find(self, **kwargs) -> typing.AsyncGenerator: async def find(self, **kwargs) -> typing.AsyncGenerator:
if not kwargs.get("_limit"): if not kwargs.get("_limit"):
kwargs["_limit"] = self.default_limit kwargs["_limit"] = self.default_limit
if not kwargs.get("deleted_at"): if not kwargs.get("deleted_at"):
kwargs["deleted_at"] = None kwargs["deleted_at"] = None
for record in await self.run_in_executor(self.table.find, **kwargs): for record in await self.db.find(self.table_name, kwargs):
model = await self.new() model = await self.new()
for key, value in record.items(): for key, value in record.items():
model[key] = value model[key] = value
@@ -88,21 +96,21 @@ class BaseMapper:
return "insert" in sql or "update" in sql or "delete" in sql return "insert" in sql or "update" in sql or "delete" in sql
async def query(self, sql, *args): async def query(self, sql, *args):
for record in await self.run_in_executor(self.db.query, sql, *args, use_semaphore=await self._use_semaphore(sql)): for record in await self.db.query(sql, *args):
yield dict(record) yield dict(record)
async def update(self, model): async def update(self, model):
if not model["deleted_at"] is None: if not model["deleted_at"] is None:
raise Exception("Can't update deleted record.") raise Exception("Can't update deleted record.")
model.updated_at.update() model.updated_at.update()
return await self.run_in_executor(self.table.update, model.record, ["uid"],use_semaphore=True) return await self.db.update(self.table_name, model.record, {"uid": model["uid"]})
async def upsert(self, model): async def upsert(self, model):
model.updated_at.update() model.updated_at.update()
return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True) await self.db.upsert(self.table_name, model.record, {"uid": model["uid"]})
return model
async def delete(self, **kwargs) -> int: async def delete(self, **kwargs) -> int:
if not kwargs or not isinstance(kwargs, dict): if not kwargs or not isinstance(kwargs, dict):
raise Exception("Can't execute delete with no filter.") raise Exception("Can't execute delete with no filter.")
kwargs["use_semaphore"] = True return await self.db.delete(self.table_name, kwargs)
return await self.run_in_executor(self.table.delete, **kwargs)
+1 -1
View File
@@ -50,7 +50,7 @@ class BaseService:
if result and result.__class__ == self.mapper.model_class: if result and result.__class__ == self.mapper.model_class:
return result return result
kwargs["uid"] = uid kwargs["uid"] = uid
print(kwargs,"ZZZZZZZ")
result = await self.mapper.get(**kwargs) result = await self.mapper.get(**kwargs)
if result: if result:
await self.cache.set(result["uid"], result) await self.cache.set(result["uid"], result)
+29 -77
View File
@@ -79,44 +79,38 @@ emoji.EMOJI_DATA[
] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]} ] = {"en": ":a1:", "status": 2, "E": 0.6, "alias": [":a1:"]}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [ ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["picture"]
"img",
"video",
"audio",
"source",
"iframe",
"picture",
"span",
]
ALLOWED_ATTRIBUTES = {
**bleach.sanitizer.ALLOWED_ATTRIBUTES,
"img": ["src", "alt", "title", "width", "height"],
"a": ["href", "title", "target", "rel", "referrerpolicy", "class"],
"iframe": [
"src",
"width",
"height",
"frameborder",
"allow",
"allowfullscreen",
"title",
"referrerpolicy",
"style",
],
"video": ["src", "controls", "width", "height"],
"audio": ["src", "controls"],
"source": ["src", "type"],
"span": ["class"],
"picture": [],
}
def sanitize_html(value): def sanitize_html(value):
soup = BeautifulSoup(value, 'html.parser')
for script in soup.find_all('script'):
script.decompose()
#for iframe in soup.find_all('iframe'):
#iframe.decompose()
for tag in soup.find_all(['object', 'embed']):
tag.decompose()
for tag in soup.find_all():
event_attributes = ['onclick', 'onerror', 'onload', 'onmouseover', 'onfocus']
for attr in event_attributes:
if attr in tag.attrs:
del tag[attr]
for img in soup.find_all('img'):
if 'onerror' in img.attrs:
img.decompose()
return soup.prettify()
def sanitize_html2(value):
return bleach.clean( return bleach.clean(
value, value,
tags=ALLOWED_TAGS, protocols=list(bleach.sanitizer.ALLOWED_PROTOCOLS) + ["data"],
attributes=ALLOWED_ATTRIBUTES,
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
strip=True, strip=True,
) )
@@ -132,50 +126,8 @@ def set_link_target_blank(text):
return str(soup) return str(soup)
SAFE_ATTRIBUTES = {
"href",
"src",
"alt",
"title",
"width",
"height",
"style",
"id",
"class",
"rel",
"type",
"name",
"value",
"placeholder",
"aria-hidden",
"aria-label",
"srcset",
"target",
"rel",
"referrerpolicy",
"controls",
"frameborder",
"allow",
"allowfullscreen",
"referrerpolicy",
}
def whitelist_attributes(html): def whitelist_attributes(html):
soup = BeautifulSoup(html, "html.parser") return sanitize_html(html)
for tag in soup.find_all():
if hasattr(tag, "attrs"):
if tag.name in ["script", "form", "input"]:
tag.replace_with("")
continue
attrs = dict(tag.attrs)
for attr in list(attrs):
# Check if attribute is in the safe list or is a data-* attribute
if not (attr in SAFE_ATTRIBUTES or attr.startswith("data-")):
del tag.attrs[attr]
return str(soup)
def embed_youtube(text): def embed_youtube(text):
+2 -2
View File
@@ -12,7 +12,7 @@ function showTerm(options){
class StarField { class StarField {
constructor({ count = 200, container = document.body } = {}) { constructor({ count = 50, container = document.body } = {}) {
this.container = container; this.container = container;
this.starCount = count; this.starCount = count;
this.stars = []; this.stars = [];
@@ -567,7 +567,7 @@ const count = Array.from(messages).filter(el => el.textContent.trim() === text).
const starField = new StarField({starCount: 200}); const starField = new StarField({starCount: 50});
app.starField = starField; app.starField = starField;
class DemoSequence { class DemoSequence {
+1 -1
View File
@@ -29,7 +29,7 @@ class ChannelDriveApiView(DriveApiView):
class ChannelAttachmentView(BaseView): class ChannelAttachmentView(BaseView):
login_required=True login_required=False
async def get(self): async def get(self):
relative_path = self.request.match_info.get("relative_url") relative_path = self.request.match_info.get("relative_url")
+1 -1
View File
@@ -11,7 +11,7 @@ import asyncio
import json import json
import logging import logging
import traceback import traceback
import random
from aiohttp import web from aiohttp import web
from snek.system.model import now from snek.system.model import now
+5 -1
View File
@@ -38,6 +38,10 @@ class WebView(BaseView):
channel = await self.services.channel.get( channel = await self.services.channel.get(
uid=self.request.match_info.get("channel") uid=self.request.match_info.get("channel")
) )
print(self.session.get("uid"),"ZZZZZZZZZZ")
qq = await self.services.user.get(uid=self.session.get("uid"))
print("GGGGGGGGGG",qq)
if not channel: if not channel:
user = await self.services.user.get( user = await self.services.user.get(
uid=self.request.match_info.get("channel") uid=self.request.match_info.get("channel")
@@ -55,7 +59,7 @@ class WebView(BaseView):
user_uid=self.session.get("uid"), channel_uid=channel["uid"] user_uid=self.session.get("uid"), channel_uid=channel["uid"]
) )
if not channel_member: if not channel_member:
if not channel["is_private"]: if not channel["is_private"] and not channel.is_dm:
channel_member = await self.app.services.channel_member.create( channel_member = await self.app.services.channel_member.create(
channel_uid=channel["uid"], channel_uid=channel["uid"],
user_uid=self.session.get("uid"), user_uid=self.session.get("uid"),
+367 -406
View File
File diff suppressed because it is too large Load Diff