Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde3819510 | ||
|
|
097889ba3f | ||
|
|
4854d40508 | ||
|
|
7dd3133475 | ||
|
|
24dfa39f91 | ||
|
|
7ec65f7c12 | ||
|
|
4f8edef42b | ||
|
|
7818410d55 | ||
|
|
1762191b03 | ||
|
|
2df92e809e | ||
|
|
59a8d32e40 | ||
|
|
c3b3963760 | ||
|
|
a0cd39e3bc |
@@ -544,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;
|
||||
}
|
||||
@@ -4,13 +4,23 @@ class ChatInputComponent extends HTMLElement {
|
||||
autoCompletions = {
|
||||
"example 1": () => {},
|
||||
"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() {
|
||||
super();
|
||||
this.lastUpdateEvent = new Date();
|
||||
this.textarea = document.createElement("textarea");
|
||||
this._value = "";
|
||||
this.value = this.getAttribute("value") || "";
|
||||
this.previousValue = this.value;
|
||||
this.lastChange = new Date();
|
||||
@@ -25,12 +35,15 @@ class ChatInputComponent extends HTMLElement {
|
||||
this._value = value || "";
|
||||
this.textarea.value = this._value;
|
||||
}
|
||||
|
||||
get allAutoCompletions() {
|
||||
return Object.assign({},this.autoCompletions,this.hiddenCompletions)
|
||||
}
|
||||
resolveAutoComplete() {
|
||||
let count = 0;
|
||||
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++;
|
||||
value = key;
|
||||
}
|
||||
@@ -149,7 +162,7 @@ levenshteinDistance(a, b) {
|
||||
const me = this;
|
||||
this.liveType = this.getAttribute("live-type") === "true";
|
||||
this.liveTypeInterval =
|
||||
parseInt(this.getAttribute("live-type-interval")) || 3;
|
||||
parseInt(this.getAttribute("live-type-interval")) || 6;
|
||||
this.channelUid = this.getAttribute("channel");
|
||||
|
||||
app.rpc.getRecentUsers(this.channelUid).then(users=>{
|
||||
@@ -188,9 +201,10 @@ levenshteinDistance(a, b) {
|
||||
|
||||
this.textarea.addEventListener("keydown", (e) => {
|
||||
this.value = e.target.value;
|
||||
let autoCompletion = null;
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
let autoCompletion = this.resolveAutoComplete();
|
||||
autoCompletion = this.resolveAutoComplete();
|
||||
if (autoCompletion) {
|
||||
e.target.value = autoCompletion;
|
||||
this.value = autoCompletion;
|
||||
@@ -206,13 +220,13 @@ levenshteinDistance(a, b) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
let autoCompletion = this.autoCompletions[message];
|
||||
if (autoCompletion) {
|
||||
this.value = "";
|
||||
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
|
||||
if (autoCompletionHandler) {
|
||||
autoCompletionHandler();
|
||||
this.value = "";
|
||||
this.previousValue = "";
|
||||
e.target.value = "";
|
||||
autoCompletion();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
DEFAULT_LIMIT = 30
|
||||
import typing
|
||||
|
||||
import asyncio
|
||||
from snek.system.model import BaseModel
|
||||
|
||||
|
||||
@@ -12,13 +12,20 @@ class BaseMapper:
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
|
||||
self.default_limit = self.__class__.default_limit
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
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):
|
||||
return self.model_class(mapper=self, app=self.app)
|
||||
|
||||
@@ -29,7 +36,8 @@ class BaseMapper:
|
||||
async def get(self, uid: str = None, **kwargs) -> BaseModel:
|
||||
if uid:
|
||||
kwargs["uid"] = uid
|
||||
record = self.table.find_one(**kwargs)
|
||||
|
||||
record = await self.run_in_executor(self.table.find_one,**kwargs)
|
||||
if not record:
|
||||
return None
|
||||
record = dict(record)
|
||||
@@ -40,31 +48,31 @@ class BaseMapper:
|
||||
return await self.model_class.from_record(mapper=self, record=record)
|
||||
|
||||
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:
|
||||
return self.table.count(**kwargs)
|
||||
return await self.run_in_executor(self.table.count, **kwargs)
|
||||
|
||||
async def save(self, model: BaseModel) -> bool:
|
||||
if not model.record.get("uid"):
|
||||
raise Exception(f"Attempt to save without uid: {model.record}.")
|
||||
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:
|
||||
if not kwargs.get("_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()
|
||||
for key, value in record.items():
|
||||
model[key] = value
|
||||
yield model
|
||||
|
||||
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)
|
||||
|
||||
async def delete(self, **kwargs) -> int:
|
||||
if not kwargs or not isinstance(kwargs, dict):
|
||||
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
@@ -1,9 +1,11 @@
|
||||
import re
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from types import SimpleNamespace
|
||||
|
||||
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 requests
|
||||
from bs4 import BeautifulSoup
|
||||
from jinja2 import TemplateSyntaxError, nodes
|
||||
from jinja2.ext import Extension
|
||||
@@ -231,6 +233,153 @@ def linkify_https(text):
|
||||
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):
|
||||
tags = {"emoji"}
|
||||
|
||||
@@ -276,6 +425,9 @@ class LinkifyExtension(Extension):
|
||||
result = embed_youtube(result)
|
||||
|
||||
result = enrich_image_rendering(result)
|
||||
|
||||
result = embed_url(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
+28
-11
@@ -157,6 +157,10 @@ app.ws.addEventListener("refresh", (data) => {
|
||||
app.starField.showNotify(data.message);
|
||||
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.starField.renderWord("Deployed", { rainbow: true, resolution: 8 });
|
||||
setTimeout(() => app.starField.shuffleAll(5000), 10000);
|
||||
@@ -169,18 +173,19 @@ app.ws.addEventListener("starfield.render_word", (data) => {
|
||||
app.addEventListener("channel-message", (data) => {
|
||||
|
||||
let display = data.text && data.text.trim() ? 'block' : 'none';
|
||||
if(data.is_final){
|
||||
if (data.channel_uid !== channelUid) {
|
||||
if (!isMentionForSomeoneElse(data.message)) {
|
||||
channelSidebar.notify(data);
|
||||
app.playSound("messageOtherChannel");
|
||||
}
|
||||
return;
|
||||
|
||||
if (data.channel_uid !== channelUid) {
|
||||
if (!isMentionForSomeoneElse(data.message)) {
|
||||
channelSidebar.notify(data);
|
||||
app.playSound("messageOtherChannel");
|
||||
}
|
||||
if (data.username !== username) {
|
||||
if (isMentionToMe(data.message)) {
|
||||
app.playSound("mention");
|
||||
} else if (!isMentionForSomeoneElse(data.message)) {
|
||||
return;
|
||||
}
|
||||
if (data.username !== username) {
|
||||
if (isMentionToMe(data.message)) {
|
||||
app.playSound("mention");
|
||||
} else if (!isMentionForSomeoneElse(data.message)) {
|
||||
if(data.is_final){
|
||||
app.playSound("message");
|
||||
}
|
||||
}
|
||||
@@ -221,6 +226,18 @@ document.addEventListener('keydown', function(event) {
|
||||
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 (!chatInputField.isActive()) {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -380,7 +380,13 @@ class RPCView(BaseView):
|
||||
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):
|
||||
scheduled = []
|
||||
@@ -407,7 +413,7 @@ class RPCView(BaseView):
|
||||
"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}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user