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 = {
|
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;
|
||||||
}
|
}
|
||||||
@@ -149,7 +162,7 @@ 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");
|
||||||
|
|
||||||
app.rpc.getRecentUsers(this.channelUid).then(users=>{
|
app.rpc.getRecentUsers(this.channelUid).then(users=>{
|
||||||
@@ -188,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;
|
||||||
@@ -206,13 +220,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -12,13 +12,20 @@ class BaseMapper:
|
|||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
self.default_limit = self.__class__.default_limit
|
self.default_limit = self.__class__.default_limit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+28
-11
@@ -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);
|
||||||
@@ -169,18 +173,19 @@ app.ws.addEventListener("starfield.render_word", (data) => {
|
|||||||
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.is_final){
|
|
||||||
if (data.channel_uid !== channelUid) {
|
if (data.channel_uid !== channelUid) {
|
||||||
if (!isMentionForSomeoneElse(data.message)) {
|
if (!isMentionForSomeoneElse(data.message)) {
|
||||||
channelSidebar.notify(data);
|
channelSidebar.notify(data);
|
||||||
app.playSound("messageOtherChannel");
|
app.playSound("messageOtherChannel");
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (data.username !== username) {
|
return;
|
||||||
if (isMentionToMe(data.message)) {
|
}
|
||||||
app.playSound("mention");
|
if (data.username !== username) {
|
||||||
} else if (!isMentionForSomeoneElse(data.message)) {
|
if (isMentionToMe(data.message)) {
|
||||||
|
app.playSound("mention");
|
||||||
|
} else if (!isMentionForSomeoneElse(data.message)) {
|
||||||
|
if(data.is_final){
|
||||||
app.playSound("message");
|
app.playSound("message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,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();
|
||||||
|
|||||||
@@ -380,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 = []
|
||||||
@@ -407,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}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user