Compare commits

..
Author SHA1 Message Date
retoor bde3819510 Update. 2025-06-01 00:24:45 +02:00
retoor 097889ba3f Make database asnyc. 2025-05-31 23:30:41 +02:00
retoor 4854d40508 Update. 2025-05-31 13:52:04 +02:00
retoor 7dd3133475 Merge pull request 'Add URL embedding functionality with metadata extraction and responsive design' (#48) from BordedDev/snek:feat/url-embedding into main
Reviewed-on: #48
Reviewed-by: retoor <retoor@noreply@molodetz.nl>
2025-05-30 19:53:22 +02:00
retoor 24dfa39f91 Update chat input. 2025-05-30 19:32:18 +02:00
BordedDev 7ec65f7c12 Cleaned up imports 2025-05-30 02:14:45 +02:00
BordedDev 4f8edef42b Add URL embedding functionality with metadata extraction and responsive design 2025-05-30 02:13:36 +02:00
retoor 7818410d55 Upddated interval. 2025-05-29 02:42:31 +02:00
retoor 1762191b03 Update. 2025-05-28 21:49:30 +02:00
retoor 2df92e809e Added iinput mode. 2025-05-28 13:40:12 +02:00
retoor 59a8d32e40 Added iinput mode. 2025-05-28 13:36:43 +02:00
retoor c3b3963760 Fix cross typing. 2025-05-28 12:31:17 +02:00
retoor a0cd39e3bc Fix cross typing. 2025-05-28 12:29:01 +02:00
retoor e48b2258e0 Made live typing default. 2025-05-28 11:38:16 +02:00
retoor 35aaf8824f Fixed directory does not exist bug. 2025-05-28 10:40:05 +02:00
retoor 76c69ca3ec Is finalized 2025-05-27 23:14:06 +02:00
retoor 9994225911 Update sort. 2025-05-27 22:22:32 +02:00
retoor 03f699e448 Updated online users. 2025-05-27 22:16:41 +02:00
retoor 2fd01a5ab7 Stars update. 2025-05-27 14:28:07 +02:00
retoor 96629113f1 Most recent users. 2025-05-27 14:03:01 +02:00
retoor 973afa0cc2 Scrolled to bottom fix. 2025-05-27 12:33:04 +02:00
retoor 9bc55e771a Scrolled to bottom fix. 2025-05-27 12:30:42 +02:00
retoor 40a292d05e Scrolled to bottom fix. 2025-05-27 12:21:40 +02:00
retoor bf723db2cc Scrolled to bottom fix. 2025-05-27 12:16:47 +02:00
retoor 46052172b2 Scrolled to bottom fix. 2025-05-27 12:14:02 +02:00
retoor 4f777f0003 Scrolled to bottom fix. 2025-05-27 12:12:03 +02:00
retoor b5e1ba72d0 Scrolled to bottom fix. 2025-05-27 12:10:26 +02:00
retoor df120098f9 Scrolled to bottom fix. 2025-05-27 12:08:53 +02:00
retoor 1c1d578db7 Fixed mentions. 2025-05-27 12:06:35 +02:00
retoor 69352fe0b5 Fixed mentions. 2025-05-27 11:45:23 +02:00
retoor fb3980dad0 Removed last-message. 2025-05-27 11:03:25 +02:00
retoor 6c21a1e619 Async load users. 2025-05-27 10:57:18 +02:00
retoor 112c0dc70a Update channel message. 2025-05-27 10:50:09 +02:00
retoor 538a9ce25d Scrolled to bottom fix. 2025-05-27 10:43:44 +02:00
17 changed files with 422 additions and 2931 deletions
-2558
View File
File diff suppressed because one or more lines are too long
-289
View File
@@ -1,289 +0,0 @@
import http.server
import socketserver
import json
import os
import subprocess
from urllib.parse import parse_qs, urlparse
import mimetypes
import html
# --- Theme selection (choose one: "light1", "light2", "dark1", "dark2") ---
THEME = "light1" # Change this to "light2", "dark1", or "dark2" as desired
THEMES = {
"light1": """
body { font-family: Arial, sans-serif; background: #f6f8fa; margin: 0; padding: 0; color: #222; }
.container { max-width: 960px; margin: auto; padding: 2em; }
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #ccc; }
.commit { margin: 1em 0; padding: 1em; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); }
.hash { color: #555; font-family: monospace; }
.diff { white-space: pre-wrap; background: #f1f1f1; padding: 1em; border-radius: 4px; overflow-x: auto; }
ul { list-style: none; padding-left: 0; }
li { margin: 0.3em 0; }
a { text-decoration: none; color: #0366d6; }
a:hover { text-decoration: underline; }
""",
"light2": """
body { font-family: 'Segoe UI', sans-serif; background: #fdf6e3; margin: 0; padding: 0; color: #333; }
.container { max-width: 900px; margin: auto; padding: 2em; }
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #e1c699; color: #b58900; }
.commit { margin: 1em 0; padding: 1em; background: #fffbe6; border-radius: 6px; box-shadow: 0 1px 3px rgba(200,180,100,0.08); }
.hash { color: #b58900; font-family: monospace; }
.diff { white-space: pre-wrap; background: #f5e9c9; padding: 1em; border-radius: 4px; overflow-x: auto; }
ul { list-style: none; padding-left: 0; }
li { margin: 0.3em 0; }
a { text-decoration: none; color: #b58900; }
a:hover { text-decoration: underline; color: #cb4b16; }
""",
"dark1": """
body { font-family: Arial, sans-serif; background: #181a1b; margin: 0; padding: 0; color: #eaeaea; }
.container { max-width: 960px; margin: auto; padding: 2em; }
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #333; color: #8ab4f8; }
.commit { margin: 1em 0; padding: 1em; background: #23272b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); }
.hash { color: #8ab4f8; font-family: monospace; }
.diff { white-space: pre-wrap; background: #23272b; padding: 1em; border-radius: 4px; overflow-x: auto; }
ul { list-style: none; padding-left: 0; }
li { margin: 0.3em 0; }
a { text-decoration: none; color: #8ab4f8; }
a:hover { text-decoration: underline; color: #bb86fc; }
""",
"dark2": """
body { font-family: 'Fira Sans', sans-serif; background: #121212; margin: 0; padding: 0; color: #d0d0d0; }
.container { max-width: 900px; margin: auto; padding: 2em; }
.date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #444; color: #ffb86c; }
.commit { margin: 1em 0; padding: 1em; background: #22223b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.22); }
.hash { color: #ffb86c; font-family: monospace; }
.diff { white-space: pre-wrap; background: #282a36; padding: 1em; border-radius: 4px; overflow-x: auto; }
ul { list-style: none; padding-left: 0; }
li { margin: 0.3em 0; }
a { text-decoration: none; color: #ffb86c; }
a:hover { text-decoration: underline; color: #8be9fd; }
""",
}
def HTML_TEMPLATE(content, theme=THEME):
return f"""
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<title>Git Log Viewer</title>
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\">
<style>
{THEMES[theme]}
</style>
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>
<script>hljs.highlightAll();</script>
</head>
<body>
<div class=\"container\">
{content}
</div>
</body>
</html>
"""
REPO_ROOT = os.path.abspath(".")
LOG_FILE = os.path.join(REPO_ROOT, "gitlog.jsonl")
PORT = 8481
def format_diff_to_html(diff_text: str) -> str:
lines = diff_text.strip().splitlines()
html_lines = ['<div style="font-family: monospace; white-space: pre;">']
while not lines[3].startswith('diff'):
lines.pop(3)
lines.insert(3, "")
for line in lines:
escaped = html.escape(line)
if "//" in line:
continue
if "#" in line:
continue
if "/*" in line:
continue
if "*/" in line:
continue
if line.startswith('+++') or line.startswith('---'):
html_lines.append(f'<div style="color: #0000aa;">{escaped}</div>')
elif line.startswith('@@'):
html_lines.append(f'<div style="color: #005cc5;">{escaped}</div>')
elif line.startswith('+'):
html_lines.append(f'<div style="color: #22863a;">{escaped}</div>')
elif line.startswith('-'):
html_lines.append(f'<div style="color: #b31d28;">{escaped}</div>')
elif line.startswith('\\'):
html_lines.append(f'<div style="color: #6a737d;">{escaped}</div>')
else:
html_lines.append(f'<div>{escaped}</div>')
html_lines.append('</div>')
return '\n'.join(html_lines)
def parse_logs():
logs = []
if not os.path.exists(LOG_FILE):
return []
lines = []
with open(LOG_FILE, "r", encoding="utf-8") as f:
for line in f:
if line.strip():
if line.strip() not in lines:
lines.append(line.strip())
logs.append(json.loads(line.strip()))
return logs
def group_by_date(logs):
grouped = {}
for entry in logs:
date = entry["date"]
grouped.setdefault(date, []).append(entry)
return dict(sorted(grouped.items(), reverse=True))
def get_git_diff(commit_hash):
try:
result = subprocess.run(
["git", "-C", REPO_ROOT, "show", commit_hash, "--no-color"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
return result.stdout
except subprocess.CalledProcessError as e:
return f"Error retrieving diff: {e.stderr}"
def list_directory(path, base_url="/browse?path="):
try:
entries = os.listdir(path)
except OSError:
return "<div>Cannot access directory.</div>"
entries.sort()
content = "<ul>"
# Parent directory link
parent = os.path.dirname(path)
if os.path.abspath(path) != REPO_ROOT:
parent_rel = os.path.relpath(parent, REPO_ROOT)
content += f"<li><a href='{base_url}{html.escape(parent_rel)}'>.. (parent directory)</a></li>"
for entry in entries:
full_path = os.path.join(path, entry)
rel_path = os.path.relpath(full_path, REPO_ROOT)
if os.path.isdir(full_path):
content += f"<li>📁 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}/</a></li>"
else:
content += f"<li>📄 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}</a></li>"
content += "</ul>"
return content
def read_file_content(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
return f"Error reading file: {e}"
def get_language_class(filename):
ext = os.path.splitext(filename)[1].lower()
return {
'.py': 'python',
'.js': 'javascript',
'.html': 'html',
'.css': 'css',
'.json': 'json',
'.sh': 'bash',
'.md': 'markdown',
'.c': 'c',
'.cpp': 'cpp',
'.h': 'cpp',
'.java': 'java',
'.rb': 'ruby',
'.go': 'go',
'.php': 'php',
'.rs': 'rust',
'.ts': 'typescript',
'.xml': 'xml',
'.yml': 'yaml',
'.yaml': 'yaml',
}.get(ext, '')
class GitLogHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == "/":
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
logs = parse_logs()
grouped = group_by_date(logs)
content = "<p><a href='/browse'>Browse Files</a></p>"
for date, commits in grouped.items():
content += f"<div class='date-header'>{date}</div>"
for c in commits:
commit_link = f"/diff?hash={c['commit']}"
content += f"""
<div class='commit'>
<div><strong>{c['line'].splitlines()[0]}</strong></div>
<div class='hash'><a href='{commit_link}'>{c['commit']}</a></div>
</div>
"""
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
elif parsed.path == "/diff":
qs = parse_qs(parsed.query)
commit = qs.get("hash", [""])[0]
diff = format_diff_to_html(get_git_diff(html.escape(commit)))
diff_html = f"<h2>Commit: {commit}</h2><div class='diff'>{diff}</div><p><a href='/'>← Back to commits</a></p>"
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(HTML_TEMPLATE(diff_html).encode("utf-8"))
elif parsed.path == "/browse":
qs = parse_qs(parsed.query)
rel_path = qs.get("path", [""])[0]
abs_path = os.path.abspath(os.path.join(REPO_ROOT, rel_path))
# Security: prevent escaping the repo root
if not abs_path.startswith(REPO_ROOT):
self.send_error(403, "Forbidden")
return
if os.path.isdir(abs_path):
content = f"<h2>Browsing: /{html.escape(rel_path)}</h2>"
content += list_directory(abs_path)
content += "<p><a href='/'>← Back to commits</a></p>"
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
elif os.path.isfile(abs_path):
file_content = read_file_content(abs_path)
lang_class = get_language_class(abs_path)
content = f"<h2>File: /{html.escape(rel_path)}</h2>"
content += (
f"<pre style='background:#f1f1f1; padding:1em; border-radius:4px; overflow-x:auto;'>"
f"<code class='{lang_class}'>{html.escape(file_content)}</code></pre>"
)
content += "<p><a href='{}'>← Back to directory</a></p>".format(
f"/browse?path={html.escape(os.path.dirname(rel_path))}"
)
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
else:
self.send_error(404, "Not found")
else:
self.send_error(404)
if __name__ == "__main__":
while True:
try:
with socketserver.TCPServer(("", PORT), GitLogHandler) as httpd:
print(f"Serving at http://localhost:{PORT}")
httpd.serve_forever()
break
except Exception as ex:
print(ex)
PORT += 1
+1
View File
@@ -7,6 +7,7 @@ class ChannelMessageModel(BaseModel):
user_uid = ModelField(name="user_uid", required=True, kind=str) user_uid = ModelField(name="user_uid", required=True, kind=str)
message = ModelField(name="message", required=True, kind=str) message = ModelField(name="message", required=True, kind=str)
html = ModelField(name="html", required=False, kind=str) html = ModelField(name="html", required=False, kind=str)
is_final = ModelField(name="is_final", required=True, kind=bool, value=True)
def get_seconds_since_last_update(self): def get_seconds_since_last_update(self):
return int((datetime.now(timezone.utc) - datetime.fromisoformat(self["updated_at"])).total_seconds()) return int((datetime.now(timezone.utc) - datetime.fromisoformat(self["updated_at"])).total_seconds())
+4
View File
@@ -67,6 +67,10 @@ class ChannelService(BaseService):
await self.services.channel_member.create_dm(channel["uid"], user1, user2) await self.services.channel_member.create_dm(channel["uid"], user1, user2)
return channel return channel
async def get_recent_users(self, channel_uid):
async for user in self.query("SELECT user.uid, user.username,user.color,user.last_ping,user.nick FROM channel_member INNER JOIN user ON user.uid = channel_member.user_uid WHERE channel_uid=:channel_uid AND user.last_ping >= datetime('now', '-3 minutes') ORDER BY last_ping DESC LIMIT 30", {"channel_uid": channel_uid}):
yield user
async def get_users(self, channel_uid): async def get_users(self, channel_uid):
async for channel_member in self.services.channel_member.find( async for channel_member in self.services.channel_member.find(
channel_uid=channel_uid, channel_uid=channel_uid,
+3 -2
View File
@@ -4,12 +4,13 @@ from snek.system.service import BaseService
class ChannelMessageService(BaseService): class ChannelMessageService(BaseService):
mapper_name = "channel_message" mapper_name = "channel_message"
async def create(self, channel_uid, user_uid, message): async def create(self, channel_uid, user_uid, message, is_final=True):
model = await self.new() model = await self.new()
model["channel_uid"] = channel_uid model["channel_uid"] = channel_uid
model["user_uid"] = user_uid model["user_uid"] = user_uid
model["message"] = message model["message"] = message
model['is_final'] = is_final
context = {} context = {}
@@ -52,7 +53,7 @@ class ChannelMessageService(BaseService):
async def save(self, model): async def save(self, model):
context = {} context = {}
content.update(model.record) context.update(model.record)
user = await self.app.services.user.get(model['user_uid']) user = await self.app.services.user.get(model['user_uid'])
context.update( context.update(
{ {
+29 -2
View File
@@ -4,12 +4,38 @@ from snek.system.service import BaseService
class ChatService(BaseService): class ChatService(BaseService):
async def send(self, user_uid, channel_uid, message): async def finalize(self, message_uid):
channel_message = await self.services.channel_message.get(uid=message_uid)
channel_message["is_final"] = True
await self.services.channel_message.save(channel_message)
user = await self.services.user.get(uid=channel_message["user_uid"])
channel = await self.services.channel.get(uid=channel_message["channel_uid"])
channel["last_message_on"] = now()
await self.services.channel.save(channel)
await self.services.socket.broadcast(
channel['uid'],
{
"message": channel_message["message"],
"html": channel_message["html"],
"user_uid": user['uid'],
"color": user["color"],
"channel_uid": channel['uid'],
"created_at": channel_message["created_at"],
"updated_at": channel_message["updated_at"],
"username": user["username"],
"uid": channel_message["uid"],
"user_nick": user["nick"],
"is_final": channel_message["is_final"],
},
)
async def send(self, user_uid, channel_uid, message, is_final=True):
channel = await self.services.channel.get(uid=channel_uid) channel = await self.services.channel.get(uid=channel_uid)
if not channel: if not channel:
raise Exception("Channel not found.") raise Exception("Channel not found.")
channel_message = await self.services.channel_message.create( channel_message = await self.services.channel_message.create(
channel_uid, user_uid, message channel_uid, user_uid, message,is_final
) )
channel_message_uid = channel_message["uid"] channel_message_uid = channel_message["uid"]
@@ -30,6 +56,7 @@ class ChatService(BaseService):
"username": user["username"], "username": user["username"],
"uid": channel_message["uid"], "uid": channel_message["uid"],
"user_nick": user["nick"], "user_nick": user["nick"],
"is_final": is_final,
}, },
) )
await self.app.create_task( await self.app.create_task(
+1 -1
View File
@@ -45,7 +45,7 @@ async def start_ssh_server(app,host,port):
logger.info("Starting SFTP server setup") logger.info("Starting SFTP server setup")
host_key_path = Path("drive") / ".ssh" / "sftp_server_key" host_key_path = Path("drive") / ".ssh" / "sftp_server_key"
host_key_path.parent.mkdir(exist_ok=True) host_key_path.parent.mkdir(exist_ok=True, parents=True)
try: try:
if not host_key_path.exists(): if not host_key_path.exists():
logger.info(f"Generating new host key at {host_key_path}") logger.info(f"Generating new host key at {host_key_path}")
+9 -3
View File
@@ -160,10 +160,14 @@ export class App extends EventHandler {
typeLock = null; typeLock = null;
typeListener = null; typeListener = null;
typeEventChannelUid = null; typeEventChannelUid = null;
_debug = false
async set_typing(channel_uid) { async set_typing(channel_uid) {
this.typeEventChannel_uid = channel_uid; this.typeEventChannel_uid = channel_uid;
} }
debug() {
this._debug = !this._debug;
this.ws._debug = this._debug;
}
async ping(...args) { async ping(...args) {
if (this.is_pinging) return false; if (this.is_pinging) return false;
this.is_pinging = true; this.is_pinging = true;
@@ -202,8 +206,10 @@ export class App extends EventHandler {
this.ws.addEventListener("channel-message", (data) => { this.ws.addEventListener("channel-message", (data) => {
me.emit("channel-message", data); me.emit("channel-message", data);
}); });
this.ws.addEventListener("event", (data) => { this.ws.addEventListener("data", (data) => {
console.info("aaaa"); if(this._debug){
console.debug(data)
}
}); });
this.rpc.getUser(null).then((user) => { this.rpc.getUser(null).then((user) => {
me.user = user; me.user = user;
+42 -7
View File
@@ -47,7 +47,6 @@ main {
} }
header { header {
background-color: #000000;
grid-area: header; grid-area: header;
padding: 10px 20px; padding: 10px 20px;
display: flex; display: flex;
@@ -102,13 +101,11 @@ h2 {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #000000;
overflow: hidden; overflow: hidden;
} }
.chat-header { .chat-header {
padding: 10px 20px; padding: 10px 20px;
background-color: #000000;
user-select: none; user-select: none;
} }
@@ -139,7 +136,6 @@ footer {
-ms-overflow-style: none; -ms-overflow-style: none;
padding: 10px; padding: 10px;
height: 10px; height: 10px;
background: #000000;
} }
.chat-messages { .chat-messages {
@@ -244,15 +240,14 @@ footer {
.chat-input { .chat-input {
padding: 15px; padding: 15px;
background-color: #000000;
display: flex; display: flex;
align-items: center; align-items: center;
} }
input[type="text"], .chat-input textarea { input[type="text"], .chat-input textarea {
flex: 1; flex: 1;
background-color: #000000;
color: white; color: white;
background: none;
border: none; border: none;
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
@@ -357,7 +352,6 @@ a {
.sidebar { .sidebar {
width: 250px; width: 250px;
background-color: #000000;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
padding-top: 10px; padding-top: 10px;
@@ -550,3 +544,44 @@ dialog .dialog-button.secondary:hover {
} }
.embed-url-link {
display: flex;
flex-direction: column;
}
.embed-url-link img,
.embed-url-link video,
.embed-url-link iframe,
.embed-url-link div {
width: auto;
height: auto;
max-width: 100%;
max-height: 400px;
object-fit: contain;
border-radius: 12px 12px 0 0;
}
.embed-url-link .page-site {
font-size: 0.9em;
color: #aaa;
margin-bottom: 5px;
}
.embed-url-link .page-name {
font-size: 1.2em;
color: #f05a28;
margin-bottom: 5px;
}
.embed-url-link .page-description {
font-size: 1em;
color: #e6e6e6;
margin-bottom: 10px;
}
.embed-url-link .page-link {
font-size: 0.9em;
color: #f05a28;
text-decoration: none;
margin-top: 10px;
}
+58 -22
View File
@@ -4,13 +4,23 @@ class ChatInputComponent extends HTMLElement {
autoCompletions = { autoCompletions = {
"example 1": () => {}, "example 1": () => {},
"example 2": () => {}, "example 2": () => {},
}; }
hiddenCompletions = {
"/starsRender": () => {
app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
}
}
users = []
textarea = null
_value = ""
lastUpdateEvent = null
previousValue = ""
lastChange = null
changed = false
constructor() { constructor() {
super(); super();
this.lastUpdateEvent = new Date(); this.lastUpdateEvent = new Date();
this.textarea = document.createElement("textarea"); this.textarea = document.createElement("textarea");
this._value = "";
this.value = this.getAttribute("value") || ""; this.value = this.getAttribute("value") || "";
this.previousValue = this.value; this.previousValue = this.value;
this.lastChange = new Date(); this.lastChange = new Date();
@@ -25,12 +35,15 @@ class ChatInputComponent extends HTMLElement {
this._value = value || ""; this._value = value || "";
this.textarea.value = this._value; this.textarea.value = this._value;
} }
get allAutoCompletions() {
return Object.assign({},this.autoCompletions,this.hiddenCompletions)
}
resolveAutoComplete() { resolveAutoComplete() {
let count = 0; let count = 0;
let value = null; let value = null;
Object.keys(this.autoCompletions).forEach((key) => {
if (key.startsWith(this.value)) { Object.keys(this.allAutoCompletions).forEach((key) => {
if (key.startsWith(this.value.split(" ")[0])) {
count++; count++;
value = key; value = key;
} }
@@ -56,7 +69,7 @@ class ChatInputComponent extends HTMLElement {
} }
extractMentions(text) { extractMentions(text) {
const regex = /@([a-zA-Z0-9_]+)/g; const regex = /@([a-zA-Z0-9_-]+)/g;
const mentions = []; const mentions = [];
let match; let match;
@@ -70,13 +83,23 @@ class ChatInputComponent extends HTMLElement {
return mentions.map(mention => { return mentions.map(mention => {
let closestAuthor = null; let closestAuthor = null;
let minDistance = Infinity; let minDistance = Infinity;
const lowerMention = mention.toLowerCase();
authors.forEach(author => { authors.forEach(author => {
const distance = this.levenshteinDistance(mention.toLowerCase(), author.toLowerCase()); const lowerAuthor = author.toLowerCase();
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
if(!this.isSubsequence(lowerMention,lowerAuthor)) {
distance += 10
}
if (distance < minDistance) { if (distance < minDistance) {
minDistance = distance; minDistance = distance;
closestAuthor = author; closestAuthor = author;
} }
}); });
return { mention, closestAuthor, distance: minDistance }; return { mention, closestAuthor, distance: minDistance };
@@ -118,11 +141,9 @@ levenshteinDistance(a, b) {
const mentions = this.extractMentions(text); const mentions = this.extractMentions(text);
const matches = this.matchMentionsToAuthors(mentions, authors); const matches = this.matchMentionsToAuthors(mentions, authors);
console.info(matches)
let updatedText = text; let updatedText = text;
matches.forEach(({ mention, closestAuthor }) => { matches.forEach(({ mention, closestAuthor }) => {
const mentionRegex = new RegExp(`@${mention}`, 'g'); const mentionRegex = new RegExp(`@${mention}`, 'g');
console.info(closestAuthor)
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`); updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
}); });
@@ -141,10 +162,12 @@ levenshteinDistance(a, b) {
const me = this; const me = this;
this.liveType = this.getAttribute("live-type") === "true"; this.liveType = this.getAttribute("live-type") === "true";
this.liveTypeInterval = this.liveTypeInterval =
parseInt(this.getAttribute("live-type-interval")) || 3; parseInt(this.getAttribute("live-type-interval")) || 6;
this.channelUid = this.getAttribute("channel"); this.channelUid = this.getAttribute("channel");
this.users = await app.rpc.getUsers(this.channelUid) app.rpc.getRecentUsers(this.channelUid).then(users=>{
this.users = users
})
this.messageUid = null; this.messageUid = null;
this.classList.add("chat-input"); this.classList.add("chat-input");
@@ -178,9 +201,10 @@ levenshteinDistance(a, b) {
this.textarea.addEventListener("keydown", (e) => { this.textarea.addEventListener("keydown", (e) => {
this.value = e.target.value; this.value = e.target.value;
let autoCompletion = null;
if (e.key === "Tab") { if (e.key === "Tab") {
e.preventDefault(); e.preventDefault();
let autoCompletion = this.resolveAutoComplete(); autoCompletion = this.resolveAutoComplete();
if (autoCompletion) { if (autoCompletion) {
e.target.value = autoCompletion; e.target.value = autoCompletion;
this.value = autoCompletion; this.value = autoCompletion;
@@ -196,18 +220,18 @@ levenshteinDistance(a, b) {
if (!message) { if (!message) {
return; return;
} }
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
let autoCompletion = this.autoCompletions[message]; if (autoCompletionHandler) {
if (autoCompletion) { autoCompletionHandler();
this.value = ""; this.value = "";
this.previousValue = ""; this.previousValue = "";
e.target.value = ""; e.target.value = "";
autoCompletion();
return; return;
} }
this.updateMessage() this.updateMessage()
app.rpc.finalizeMessage(this.messageUid)
this.value = ""; this.value = "";
this.previousValue = ""; this.previousValue = "";
this.messageUid = null; this.messageUid = null;
@@ -239,7 +263,7 @@ levenshteinDistance(a, b) {
e.detail.files.forEach((file) => { e.detail.files.forEach((file) => {
message += `[${file.name}](/channel/attachment/${file.relative_url})`; message += `[${file.name}](/channel/attachment/${file.relative_url})`;
}); });
app.rpc.sendMessage(this.channelUid, message); app.rpc.sendMessage(this.channelUid, message,true);
}); });
setTimeout(()=>{ setTimeout(()=>{
this.focus(); this.focus();
@@ -250,13 +274,25 @@ levenshteinDistance(a, b) {
const millisecondsDifference = event2Time.getTime() - event1Time.getTime(); const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
return millisecondsDifference / 1000; return millisecondsDifference / 1000;
} }
isSubsequence(s, t) {
let i = 0, j = 0;
while (i < s.length && j < t.length) {
if (s[i] === t[j]) {
i++;
}
j++;
}
return i === s.length;
}
newMessage() { newMessage() {
if (!this.messageUid) { if (!this.messageUid) {
this.messageUid = "?"; this.messageUid = "?";
} }
this.sendMessage(this.channelUid, this.value).then((uid) => { this.value = this.replaceMentionsWithAuthors(this.value);
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
if (this.liveType) { if (this.liveType) {
this.messageUid = uid; this.messageUid = uid;
} }
@@ -326,11 +362,11 @@ levenshteinDistance(a, b) {
} }
} }
async sendMessage(channelUid, value) { async sendMessage(channelUid, value,is_final) {
if (!value.trim()) { if (!value.trim()) {
return null; return null;
} }
return await app.rpc.sendMessage(channelUid, value); return await app.rpc.sendMessage(channelUid, value,is_final);
} }
} }
+15 -3
View File
@@ -38,9 +38,19 @@ class MessageList extends HTMLElement {
}) })
} }
isElementVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
isScrolledToBottom() {
return this.isElementVisible(this.querySelector(".message-list-bottom"));
}
scrollToBottom(force) { scrollToBottom(force) {
console.info("Scrolling down")
// if (force) {
this.scrollTop = this.scrollHeight; this.scrollTop = this.scrollHeight;
this.querySelector(".message-list-bottom").scrollIntoView(); this.querySelector(".message-list-bottom").scrollIntoView();
@@ -49,7 +59,6 @@ class MessageList extends HTMLElement {
this.scrollTop = this.scrollHeight; this.scrollTop = this.scrollHeight;
this.querySelector(".message-list-bottom").scrollIntoView(); this.querySelector(".message-list-bottom").scrollIntoView();
},200) },200)
// }
} }
updateMessageText(uid, message) { updateMessageText(uid, message) {
const messageDiv = this.querySelector('div[data-uid="' + uid + '"]'); const messageDiv = this.querySelector('div[data-uid="' + uid + '"]');
@@ -57,12 +66,15 @@ class MessageList extends HTMLElement {
if (!messageDiv) { if (!messageDiv) {
return; return;
} }
const scrollToBottom = this.isScrolledToBottom();
const receivedHtml = document.createElement("div"); const receivedHtml = document.createElement("div");
receivedHtml.innerHTML = message.html; receivedHtml.innerHTML = message.html;
const html = receivedHtml.querySelector(".text").innerHTML; const html = receivedHtml.querySelector(".text").innerHTML;
const textElement = messageDiv.querySelector(".text"); const textElement = messageDiv.querySelector(".text");
textElement.innerHTML = html; textElement.innerHTML = html;
textElement.style.display = message.text == "" ? "none" : "block"; textElement.style.display = message.text == "" ? "none" : "block";
if(scrollToBottom)
this.scrollToBottom(true)
} }
triggerGlow(uid,color) { triggerGlow(uid,color) {
app.starField.glowColor(color) app.starField.glowColor(color)
+1
View File
@@ -25,6 +25,7 @@
border-radius: 50%; border-radius: 50%;
background-color: var(--star-color); background-color: var(--star-color);
animation: twinkle 2s infinite ease-in-out; animation: twinkle 2s infinite ease-in-out;
z-index: -1;
} }
@keyframes twinkle { @keyframes twinkle {
+7
View File
@@ -17,6 +17,8 @@ export class Socket extends EventHandler {
shouldReconnect = true; shouldReconnect = true;
_debug = false;
get isConnected() { get isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN; return this.ws && this.ws.readyState === WebSocket.OPEN;
} }
@@ -112,6 +114,11 @@ export class Socket extends EventHandler {
get(_, prop) { get(_, prop) {
return (...args) => { return (...args) => {
const functionName = me._camelToSnake(prop); const functionName = me._camelToSnake(prop);
if(me._debug){
const call = {}
call[functionName] = args
console.debug(call)
}
return me.call(functionName, ...args); return me.call(functionName, ...args);
}; };
}, },
+16 -8
View File
@@ -1,6 +1,6 @@
DEFAULT_LIMIT = 30 DEFAULT_LIMIT = 30
import typing import typing
import asyncio
from snek.system.model import BaseModel from snek.system.model import BaseModel
@@ -19,6 +19,13 @@ class BaseMapper:
def db(self): def db(self):
return self.app.db return self.app.db
@property
def loop(self):
return asyncio.get_event_loop()
async def run_in_executor(self, func, *args, **kwargs):
return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
async def new(self): async def new(self):
return self.model_class(mapper=self, app=self.app) return self.model_class(mapper=self, app=self.app)
@@ -29,7 +36,8 @@ class BaseMapper:
async def get(self, uid: str = None, **kwargs) -> BaseModel: async def get(self, uid: str = None, **kwargs) -> BaseModel:
if uid: if uid:
kwargs["uid"] = uid kwargs["uid"] = uid
record = self.table.find_one(**kwargs)
record = await self.run_in_executor(self.table.find_one,**kwargs)
if not record: if not record:
return None return None
record = dict(record) record = dict(record)
@@ -40,31 +48,31 @@ class BaseMapper:
return await self.model_class.from_record(mapper=self, record=record) return await self.model_class.from_record(mapper=self, record=record)
async def exists(self, **kwargs): async def exists(self, **kwargs):
return self.table.exists(**kwargs) return await self.run_in_executor(self.table.exists,**kwargs)
async def count(self, **kwargs) -> int: async def count(self, **kwargs) -> int:
return self.table.count(**kwargs) return await self.run_in_executor(self.table.count, **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 self.table.upsert(model.record, ["uid"]) return await self.run_in_executor(self.table.upsert, model.record, ["uid"])
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
for record in self.table.find(**kwargs): for record in await self.run_in_executor(self.table.find, **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
yield model yield model
async def query(self, sql, *args): async def query(self, sql, *args):
for record in self.db.query(sql, *args): for record in await self.run_in_executor(self.db.query,sql, *args):
yield dict(record) yield dict(record)
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.")
return self.table.delete(**kwargs) return await self.run_in_executor(self.table.delete, **kwargs)
+156 -4
View File
@@ -1,9 +1,11 @@
import re
from urllib.parse import urlparse, parse_qs
from types import SimpleNamespace
import mimetypes import mimetypes
import re
from functools import lru_cache
from types import SimpleNamespace
from urllib.parse import urlparse, parse_qs
from app.cache import time_cache
import emoji import emoji
import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from jinja2 import TemplateSyntaxError, nodes from jinja2 import TemplateSyntaxError, nodes
from jinja2.ext import Extension from jinja2.ext import Extension
@@ -231,6 +233,153 @@ def linkify_https(text):
return set_link_target_blank(str(soup)) return set_link_target_blank(str(soup))
@time_cache(timeout=60*60)
def get_url_content(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Error fetching {url}: {e}")
return None
def embed_url(text):
soup = BeautifulSoup(text, "html.parser")
attachments = {}
for element in soup.find_all("a"):
if "href" in element.attrs and element.attrs["href"].startswith("http"):
page_url = urlparse(element.attrs["href"])
page = get_url_content(element.attrs["href"])
if page:
parsed_page = BeautifulSoup(page, "html.parser")
head_info = parsed_page.find("head")
if head_info:
def get_element_options(
elem=None, meta=None, ograph=None, twitter=None
):
if twitter:
tw_tag = head_info.find(
"meta", attrs={"name": "twitter:" + twitter}
) or head_info.find(
"meta", attrs={"property": "twitter:" + twitter}
)
if tw_tag:
return tw_tag.get("content", tw_tag.get("value", None))
if ograph:
og_tag = head_info.find(
"meta", attrs={"property": "og:" + ograph}
) or head_info.find("meta", attrs={"name": "og:" + ograph})
if og_tag:
return og_tag.get("content", og_tag.get("value", None))
if meta:
meta_tag = head_info.find(
"meta", attrs={"name": meta}
) or head_info.find("meta", attrs={"property": meta})
if meta_tag:
return meta_tag.get(
"content", meta_tag.get("value", None)
)
if elem:
elem_tag = head_info.find(elem)
if elem_tag:
return elem_tag.text
return None
original_link_name = element.attrs["href"]
if original_link_name in attachments:
continue
page_name = (
get_element_options("title", "title", "title", "title")
or page_url.netloc
)
page_site = (
get_element_options(None, "site", "site", "site")
or page_url.netloc
)
page_description = get_element_options(
None, "description", "description", "description"
)
page_image = get_element_options(None, "image", "image", "image")
page_image_alt = get_element_options(
None, "image:alt", "image:alt", "image:alt"
)
page_video = get_element_options(None, "video", "video", "video")
page_audio = get_element_options(None, "audio", "audio", "audio")
preview_size = (
get_element_options(None, None, None, "card")
or "summary_large_image"
)
attachment_base = BeautifulSoup(str(element), "html.parser")
attachments[original_link_name] = attachment_base
attachment = next(attachment_base.children)
attachment.clear()
attachment.attrs["class"] = "embed-url-link"
render_element = attachment
if page_image:
image_template = f'<span><img src="{page_image}" alt="{page_image_alt or page_name}" title="{page_name}" width="420" height="240" /></span>'
render_element.append(
BeautifulSoup(image_template, "html.parser")
)
if page_video:
video_template = f'<video controls><source src="{page_video}">Your browser does not support the video tag.</video>'
render_element.append(
BeautifulSoup(video_template, "html.parser")
)
if page_audio:
audio_template = f'<audio controls><source src="{page_audio}">Your browser does not support the audio tag.</audio>'
render_element.append(
BeautifulSoup(audio_template, "html.parser")
)
description_element_base = BeautifulSoup(
"<span class='description'></span>", "html.parser"
)
description_element = next(description_element_base.children)
description_element.append(
BeautifulSoup(
f'<p class="page-site">{page_site}</p>',
"html.parser",
)
)
description_element.append(
BeautifulSoup(f'<strong class="page-name">{page_name}</strong>', "html.parser")
)
description_element.append(
BeautifulSoup(f"<p class='page-description'>{page_description or "No description available."}</p>", "html.parser")
)
description_element.append(
BeautifulSoup(f"<p class='page-original-link'>{original_link_name}</p>", "html.parser")
)
render_element.append(description_element_base)
for attachment in attachments.values():
soup.append(attachment)
return str(soup)
class EmojiExtension(Extension): class EmojiExtension(Extension):
tags = {"emoji"} tags = {"emoji"}
@@ -276,6 +425,9 @@ class LinkifyExtension(Extension):
result = embed_youtube(result) result = embed_youtube(result)
result = enrich_image_rendering(result) result = enrich_image_rendering(result)
result = embed_url(result)
return result return result
+31 -14
View File
@@ -13,7 +13,7 @@
{% endfor %} {% endfor %}
<div class="message-list-bottom"></div> <div class="message-list-bottom"></div>
</message-list> </message-list>
<chat-input live-type="false" channel="{{ channel.uid.value }}"></chat-input> <chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
</section> </section>
{% include "dialog_help.html" %} {% include "dialog_help.html" %}
{% include "dialog_online.html" %} {% include "dialog_online.html" %}
@@ -157,6 +157,10 @@ app.ws.addEventListener("refresh", (data) => {
app.starField.showNotify(data.message); app.starField.showNotify(data.message);
setTimeout(() => window.location.reload(), 4000); setTimeout(() => window.location.reload(), 4000);
}); });
app.ws.addEventListener("stars_render", (data)=>{
app.starField.renderWord(data.message, { rainbow: true, resolution: 8 });
setTimeout(() => app.starField.shuffleAll(5000), 10000);
})
app.ws.addEventListener("deployed", (data) => { app.ws.addEventListener("deployed", (data) => {
app.starField.renderWord("Deployed", { rainbow: true, resolution: 8 }); app.starField.renderWord("Deployed", { rainbow: true, resolution: 8 });
setTimeout(() => app.starField.shuffleAll(5000), 10000); setTimeout(() => app.starField.shuffleAll(5000), 10000);
@@ -167,7 +171,9 @@ app.ws.addEventListener("starfield.render_word", (data) => {
// --- Channel message event --- // --- Channel message event ---
app.addEventListener("channel-message", (data) => { app.addEventListener("channel-message", (data) => {
let display = data.text && data.text.trim() ? 'block' : 'none'; let display = data.text && data.text.trim() ? 'block' : 'none';
if (data.channel_uid !== channelUid) { if (data.channel_uid !== channelUid) {
if (!isMentionForSomeoneElse(data.message)) { if (!isMentionForSomeoneElse(data.message)) {
channelSidebar.notify(data); channelSidebar.notify(data);
@@ -179,13 +185,22 @@ app.addEventListener("channel-message", (data) => {
if (isMentionToMe(data.message)) { if (isMentionToMe(data.message)) {
app.playSound("mention"); app.playSound("mention");
} else if (!isMentionForSomeoneElse(data.message)) { } else if (!isMentionForSomeoneElse(data.message)) {
if(data.is_final){
app.playSound("message"); app.playSound("message");
} }
} }
const lastMessage = messagesContainer.querySelector(".message:last-child"); }
const lastElement = messagesContainer.querySelector(".message-list-bottom"); const lastElement = messagesContainer.querySelector(".message-list-bottom");
const doScrollDown = !lastMessage || isElementVisible(lastMessage); const doScrollDown = messagesContainer.isScrolledToBottom();
const oldMessage = messagesContainer.querySelector(`.message[data-uid="${data.uid}"]`);
if (oldMessage) {
oldMessage.remove();
}
const message = document.createElement("div"); const message = document.createElement("div");
message.innerHTML = data.html; message.innerHTML = data.html;
message.style.display = display; message.style.display = display;
messagesContainer.insertBefore(message.firstChild, lastElement); messagesContainer.insertBefore(message.firstChild, lastElement);
@@ -211,6 +226,18 @@ document.addEventListener('keydown', function(event) {
loadExtra(); loadExtra();
} }
} }
if(event.key == 'i' && !chatInputField.isActive()) {
event.preventDefault();
chatInputField.focus();
}
if(event.key == 'r' && !chatInputField.isActive()) {
event.preventDefault();
const replyLinks = document.querySelectorAll('.time a')
if(replyLinks.length){
const replyLink = replyLinks[replyLinks.length - 1]
replyLink.click()
}
}
if (event.shiftKey && event.key === 'G') { if (event.shiftKey && event.key === 'G') {
if (!chatInputField.isActive()) { if (!chatInputField.isActive()) {
event.preventDefault(); event.preventDefault();
@@ -222,7 +249,6 @@ document.addEventListener('keydown', function(event) {
// --- Image click-to-zoom (delegated) --- // --- Image click-to-zoom (delegated) ---
// --- Layout update --- // --- Layout update ---
let lastMessage;
function updateLayout(doScrollDown) { function updateLayout(doScrollDown) {
updateTimes(); updateTimes();
let previousUser = null, previousDate = null; let previousUser = null, previousDate = null;
@@ -246,20 +272,11 @@ function updateLayout(doScrollDown) {
} }
} }
}); });
lastMessage = messagesContainer.querySelector(".message:last-child");
if (doScrollDown) messagesContainer.scrollToBottom?.(); if (doScrollDown) messagesContainer.scrollToBottom?.();
} }
// --- Utility: check if element is visible --- // --- Utility: check if element is visible ---
function isElementVisible(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
// --- Utility: check if scrolled past half --- // --- Utility: check if scrolled past half ---
function isScrolledPastHalf() { function isScrolledPastHalf() {
+40 -9
View File
@@ -193,6 +193,21 @@ class RPCView(BaseView):
) )
return channels return channels
async def finalize_message(self, message_uid):
self._require_login()
message = await self.services.channel_message.get(message_uid)
if not message:
return False
if message["user_uid"] != self.user_uid:
raise Exception("Not allowed")
if not message['is_final']:
await self.services.chat.finalize(message['uid'])
return True
async def update_message_text(self,message_uid, text): async def update_message_text(self,message_uid, text):
self._require_login() self._require_login()
message = await self.services.channel_message.get(message_uid) message = await self.services.channel_message.get(message_uid)
@@ -200,6 +215,7 @@ class RPCView(BaseView):
raise Exception("Not allowed") raise Exception("Not allowed")
if message.get_seconds_since_last_update() > 3: if message.get_seconds_since_last_update() > 3:
await self.finalize_message(message["uid"])
return {"error": "Message too old","seconds_since_last_update": message.get_seconds_since_last_update(),"success": False} return {"error": "Message too old","seconds_since_last_update": message.get_seconds_since_last_update(),"success": False}
message['message'] = text message['message'] = text
@@ -221,9 +237,9 @@ class RPCView(BaseView):
return {"success": True} return {"success": True}
async def send_message(self, channel_uid, message): async def send_message(self, channel_uid, message,is_final=True):
self._require_login() self._require_login()
message = await self.services.chat.send(self.user_uid, channel_uid, message) message = await self.services.chat.send(self.user_uid, channel_uid, message,is_final)
return message["uid"] return message["uid"]
@@ -314,19 +330,28 @@ class RPCView(BaseView):
self._require_login() self._require_login()
results = [ results = [
record.record async for record in self.services.channel.get_online_users(channel_uid) record async for record in self.services.channel.get_recent_users(channel_uid)
] ]
for result in results: results = sorted(results, key=lambda x: x["nick"])
del result['email']
del result['password']
del result['deleted_at']
del result['updated_at']
return results return results
async def echo(self, obj): async def echo(self, obj):
await self.ws.send_json(obj) await self.ws.send_json(obj)
return "noresponse" return "noresponse"
async def get_recent_users(self, channel_uid):
self._require_login()
return [
{
"uid": record["uid"],
"username": record["username"],
"nick": record["nick"],
"last_ping": record["last_ping"],
}
async for record in self.services.channel.get_recent_users(channel_uid)
]
async def get_users(self, channel_uid): async def get_users(self, channel_uid):
self._require_login() self._require_login()
@@ -355,7 +380,13 @@ class RPCView(BaseView):
return {"pong": args} return {"pong": args}
async def stars_render(self, channel_uid, message):
for user in await self.get_online_users(channel_uid):
try:
await self.services.socket.send_to_user(user['uid'], dict(event="stars_render", data={"channel_uid": channel_uid, "message":message}))
except Exception as ex:
print(ex)
async def get(self): async def get(self):
scheduled = [] scheduled = []
@@ -382,7 +413,7 @@ class RPCView(BaseView):
"message": "Finishing deployment"} "message": "Finishing deployment"}
} }
) )
await schedule(self.request.session.get("uid"),10,{"event": "deployed", "data": { await schedule(self.request.session.get("uid"),15,{"event": "deployed", "data": {
"uptime": self.request.app.uptime} "uptime": self.request.app.uptime}
} }
) )