forked from retoor/snek
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6337350b60 | ||
|
|
986acfac38 | ||
|
|
b27149b5ba | ||
|
|
eb1284060a | ||
|
|
4266ac1f12 | ||
|
|
6b4709d011 | ||
|
|
ef8d3068a8 | ||
|
|
a23c14389b | ||
|
|
6dfd8db0a6 | ||
|
|
abce2e03d1 | ||
|
|
54d7d5b74e | ||
|
|
17bb88050a | ||
|
|
8c2e20dfe8 | ||
|
|
3e2dd7ea04 | ||
|
|
70eebefac7 | ||
|
|
ac47d201d8 | ||
|
|
11e19f48e8 | ||
|
|
5ac49522d9 | ||
|
|
f9f1179db5 | ||
|
|
04527c286f | ||
|
|
e23d6571c8 | ||
|
|
0c331bbb93 | ||
|
|
a2d506cce9 |
@@ -31,6 +31,7 @@ from snek.sgit import GitApplication
|
|||||||
from snek.sssh import start_ssh_server
|
from snek.sssh import start_ssh_server
|
||||||
from snek.system import http
|
from snek.system import http
|
||||||
from snek.system.cache import Cache
|
from snek.system.cache import Cache
|
||||||
|
from snek.system.stats import middleware as stats_middleware, create_stats_structure, stats_handler
|
||||||
from snek.system.markdown import MarkdownExtension
|
from snek.system.markdown import MarkdownExtension
|
||||||
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
|
from snek.system.middleware import auth_middleware, cors_middleware, csp_middleware
|
||||||
from snek.system.profiler import profiler_handler
|
from snek.system.profiler import profiler_handler
|
||||||
@@ -127,6 +128,7 @@ async def trailing_slash_middleware(request, handler):
|
|||||||
class Application(BaseApplication):
|
class Application(BaseApplication):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
middlewares = [
|
middlewares = [
|
||||||
|
stats_middleware,
|
||||||
cors_middleware,
|
cors_middleware,
|
||||||
web.normalize_path_middleware(merge_slashes=True),
|
web.normalize_path_middleware(merge_slashes=True),
|
||||||
ip2location_middleware,
|
ip2location_middleware,
|
||||||
@@ -168,11 +170,17 @@ class Application(BaseApplication):
|
|||||||
self.ip2location = IP2Location.IP2Location(
|
self.ip2location = IP2Location.IP2Location(
|
||||||
base_path.joinpath("IP2LOCATION-LITE-DB11.BIN")
|
base_path.joinpath("IP2LOCATION-LITE-DB11.BIN")
|
||||||
)
|
)
|
||||||
|
self.on_startup.append(self.prepare_stats)
|
||||||
self.on_startup.append(self.prepare_asyncio)
|
self.on_startup.append(self.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):
|
||||||
|
app['stats'] = create_stats_structure()
|
||||||
|
print("Stats prepared", flush=True)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uptime_seconds(self):
|
def uptime_seconds(self):
|
||||||
return (datetime.now() - self.time_start).total_seconds()
|
return (datetime.now() - self.time_start).total_seconds()
|
||||||
@@ -308,6 +316,7 @@ class Application(BaseApplication):
|
|||||||
self.router.add_view("/drive.json", DriveApiView)
|
self.router.add_view("/drive.json", DriveApiView)
|
||||||
self.router.add_view("/drive.html", DriveView)
|
self.router.add_view("/drive.html", DriveView)
|
||||||
self.router.add_view("/drive/{drive}.json", DriveView)
|
self.router.add_view("/drive/{drive}.json", DriveView)
|
||||||
|
self.router.add_get("/stats.html", stats_handler)
|
||||||
self.router.add_view("/stats.json", StatsView)
|
self.router.add_view("/stats.json", StatsView)
|
||||||
self.router.add_view("/user/{user}.html", UserView)
|
self.router.add_view("/user/{user}.html", UserView)
|
||||||
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
|
self.router.add_view("/repository/{username}/{repository}", RepositoryView)
|
||||||
|
|||||||
@@ -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"]:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
mapper_name = "channel_message"
|
mapper_name = "channel_message"
|
||||||
@@ -11,21 +11,29 @@ class ChannelMessageService(BaseService):
|
|||||||
|
|
||||||
async def maintenance(self):
|
async def maintenance(self):
|
||||||
args = {}
|
args = {}
|
||||||
async for message in self.find():
|
for message in self.mapper.db["channel_message"].find():
|
||||||
updated_at = message["updated_at"]
|
print(message)
|
||||||
message["is_final"] = True
|
try:
|
||||||
html = message["html"]
|
message = await self.get(uid=message["uid"])
|
||||||
await self.save(message)
|
updated_at = message["updated_at"]
|
||||||
|
message["is_final"] = True
|
||||||
|
html = message["html"]
|
||||||
|
await self.save(message)
|
||||||
|
|
||||||
|
self.mapper.db["channel_message"].upsert(
|
||||||
|
{
|
||||||
|
"uid": message["uid"],
|
||||||
|
"updated_at": updated_at,
|
||||||
|
},
|
||||||
|
["uid"],
|
||||||
|
)
|
||||||
|
if html != message["html"]:
|
||||||
|
print("Reredefined message", message["uid"])
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(ex, flush=True)
|
||||||
|
|
||||||
self.mapper.db["channel_message"].upsert(
|
|
||||||
{
|
|
||||||
"uid": message["uid"],
|
|
||||||
"updated_at": updated_at,
|
|
||||||
},
|
|
||||||
["uid"],
|
|
||||||
)
|
|
||||||
if html != message["html"]:
|
|
||||||
print("Reredefined message", message["uid"])
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
changed = 0
|
changed = 0
|
||||||
@@ -64,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)
|
||||||
|
|
||||||
@@ -91,9 +99,9 @@ class ChannelMessageService(BaseService):
|
|||||||
if not user:
|
if not user:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
if not message["html"].startswith("<chat-message"):
|
#if not message["html"].startswith("<chat-message"):
|
||||||
await (await self.get(uid=message["uid"])).save()
|
#message = await self.get(uid=message["uid"])
|
||||||
message["html"] = (await self.get(uid=message["uid"])).html
|
#await self.save(message)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"uid": message["uid"],
|
"uid": message["uid"],
|
||||||
@@ -121,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):
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ input[type="text"], .chat-input textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.switch-user + .message, .message.long-time + .message, .message:first-child {
|
.message.switch-user + .message, .message.long-time + .message, .message-list-bottom + .message{
|
||||||
.time {
|
.time {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,53 @@ import {app} from "./app.js";
|
|||||||
|
|
||||||
const LONG_TIME = 1000 * 60 * 20
|
const LONG_TIME = 1000 * 60 * 20
|
||||||
|
|
||||||
class MessageElement extends HTMLElement {
|
export class ReplyEvent extends Event {
|
||||||
static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
constructor(messageTextTarget) {
|
||||||
|
super('reply', { bubbles: true, composed: true });
|
||||||
|
this.messageTextTarget = messageTextTarget;
|
||||||
|
|
||||||
isVisible() {
|
const newMessage = messageTextTarget.cloneNode(true);
|
||||||
if (!this) return false;
|
newMessage.style.maxHeight = "0"
|
||||||
const rect = this.getBoundingClientRect();
|
messageTextTarget.parentElement.insertBefore(newMessage, messageTextTarget);
|
||||||
return (
|
|
||||||
rect.top >= 0 &&
|
newMessage.querySelectorAll('.embed-url-link').forEach(link => {
|
||||||
rect.left >= 0 &&
|
link.remove()
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
})
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
||||||
);
|
newMessage.querySelectorAll('picture').forEach(picture => {
|
||||||
}
|
const img = picture.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
picture.replaceWith(img);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
newMessage.querySelectorAll('img').forEach(img => {
|
||||||
|
const src = img.src || img.currentSrc;
|
||||||
|
img.replaceWith(document.createTextNode(src));
|
||||||
|
})
|
||||||
|
|
||||||
|
newMessage.querySelectorAll('iframe').forEach(iframe => {
|
||||||
|
const src = iframe.src || iframe.currentSrc;
|
||||||
|
iframe.replaceWith(document.createTextNode(src));
|
||||||
|
})
|
||||||
|
|
||||||
|
newMessage.querySelectorAll('a').forEach(a => {
|
||||||
|
const href = a.getAttribute('href');
|
||||||
|
const text = a.innerText || a.textContent;
|
||||||
|
if (text === href || text === '') {
|
||||||
|
a.replaceWith(document.createTextNode(href));
|
||||||
|
} else {
|
||||||
|
a.replaceWith(document.createTextNode(`[${text}](${href})`));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.replyText = newMessage.innerText.replaceAll("\n\n", "\n").trim();
|
||||||
|
newMessage.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessageElement extends HTMLElement {
|
||||||
|
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
if (this._originalChildren === undefined) {
|
if (this._originalChildren === undefined) {
|
||||||
@@ -51,10 +85,16 @@ class MessageElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.timeDiv = this.querySelector('.time span');
|
this.timeDiv = this.querySelector('.time span');
|
||||||
|
this.replyDiv = this.querySelector('.time a');
|
||||||
|
|
||||||
|
this.replyDiv.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.dispatchEvent(new ReplyEvent(this.messageDiv));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.siblingGenerated && this.nextElementSibling) {
|
if ((!this.siblingGenerated || this.siblingGenerated !== this.nextElementSibling) && this.nextElementSibling) {
|
||||||
this.siblingGenerated = true;
|
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 {
|
||||||
@@ -99,7 +139,9 @@ class MessageList extends HTMLElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
app.ws.addEventListener("update_message_text", (data) => {
|
app.ws.addEventListener("update_message_text", (data) => {
|
||||||
this.upsertMessage(data);
|
if (this.messageMap.has(data.uid)) {
|
||||||
|
this.upsertMessage(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
app.ws.addEventListener("set_typing", (data) => {
|
app.ws.addEventListener("set_typing", (data) => {
|
||||||
this.triggerGlow(data.user_uid,data.color);
|
this.triggerGlow(data.user_uid,data.color);
|
||||||
@@ -108,29 +150,33 @@ class MessageList extends HTMLElement {
|
|||||||
this.messageMap = new Map();
|
this.messageMap = new Map();
|
||||||
this.visibleSet = new Set();
|
this.visibleSet = new Set();
|
||||||
this._observer = new IntersectionObserver((entries) => {
|
this._observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.visibleSet.add(entry.target);
|
this.visibleSet.add(entry.target);
|
||||||
const messageElement = entry.target;
|
const messageElement = entry.target;
|
||||||
if (messageElement instanceof MessageElement) {
|
if (messageElement instanceof MessageElement) {
|
||||||
messageElement.updateUI();
|
messageElement.updateUI();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.visibleSet.delete(entry.target);
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
console.log(this.visibleSet);
|
this.visibleSet.delete(entry.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, {
|
}, {
|
||||||
root: this,
|
root: this,
|
||||||
threshold: 0.1
|
threshold: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.endOfMessages = document.createElement('div');
|
||||||
|
this.endOfMessages.classList.add('message-list-bottom');
|
||||||
|
this.prepend(this.endOfMessages);
|
||||||
|
|
||||||
for(const c of this.children) {
|
for(const c of this.children) {
|
||||||
this._observer.observe(c);
|
this._observer.observe(c);
|
||||||
if (c instanceof MessageElement) {
|
if (c instanceof MessageElement) {
|
||||||
this.messageMap.set(c.dataset.uid, c);
|
this.messageMap.set(c.dataset.uid, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollToBottom(true);
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +220,6 @@ class MessageList extends HTMLElement {
|
|||||||
};
|
};
|
||||||
document.addEventListener('keydown', escListener);
|
document.addEventListener('keydown', escListener);
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
isElementVisible(element) {
|
isElementVisible(element) {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
@@ -187,13 +232,13 @@ class MessageList extends HTMLElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.isElementVisible(this.firstElementChild);
|
return this.visibleSet.has(this.endOfMessages)
|
||||||
}
|
}
|
||||||
scrollToBottom(force = false, behavior= 'smooth') {
|
scrollToBottom(force = false, behavior= 'instant') {
|
||||||
if (force || this.isScrolledToBottom()) {
|
if (force || !this.isScrolledToBottom()) {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'start' });
|
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,9 +272,8 @@ class MessageList extends HTMLElement {
|
|||||||
|
|
||||||
upsertMessage(data) {
|
upsertMessage(data) {
|
||||||
let message = this.messageMap.get(data.uid);
|
let message = this.messageMap.get(data.uid);
|
||||||
const newMessage = !!message;
|
|
||||||
if (message) {
|
if (message) {
|
||||||
message.parentElement.removeChild(message);
|
message.parentElement?.removeChild(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.message) return
|
if (!data.message) return
|
||||||
@@ -239,16 +283,16 @@ class MessageList extends HTMLElement {
|
|||||||
wrapper.innerHTML = data.html;
|
wrapper.innerHTML = data.html;
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
message.updateMessage(...wrapper.firstElementChild._originalChildren);
|
message.updateMessage(...(wrapper.firstElementChild._originalChildren || wrapper.firstElementChild.children));
|
||||||
} else {
|
} else {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrolledToBottom = this.isScrolledToBottom();
|
const scrolledToBottom = this.isScrolledToBottom();
|
||||||
this.prepend(message);
|
this.endOfMessages.after(message);
|
||||||
if (scrolledToBottom) this.scrollToBottom(true, !newMessage ? 'smooth' : 'auto');
|
if (scrolledToBottom) this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+40
-14
@@ -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);
|
||||||
}
|
}
|
||||||
this.config.items.forEach(item => this.appendChild(item));
|
if (this.config.items) {
|
||||||
|
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',
|
||||||
@@ -493,7 +518,7 @@ document.body.appendChild(dialog);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class NjetComponent extends Component {}
|
class NjetComponent extends Component {}
|
||||||
const njet = Njet
|
const njet = Njet
|
||||||
njet.showDialog = function(args){
|
njet.showDialog = function(args){
|
||||||
const dialog = new NjetDialog(args)
|
const dialog = new NjetDialog(args)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
@@ -545,15 +570,16 @@ 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] = []
|
||||||
}
|
}
|
||||||
this.root._subscriptions[event].push(callback)
|
this.root._subscriptions[event].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow,eventBus };
|
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow, eventBus };
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import asyncio
|
||||||
|
from aiohttp import web, WSMsgType
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from collections import defaultdict
|
||||||
|
import html
|
||||||
|
|
||||||
|
def create_stats_structure():
|
||||||
|
"""Creates the nested dictionary structure for storing statistics."""
|
||||||
|
def nested_dd():
|
||||||
|
return defaultdict(lambda: defaultdict(int))
|
||||||
|
return defaultdict(nested_dd)
|
||||||
|
|
||||||
|
def get_time_keys(dt: datetime):
|
||||||
|
"""Generates dictionary keys for different time granularities."""
|
||||||
|
return {
|
||||||
|
"hour": dt.strftime('%Y-%m-%d-%H'),
|
||||||
|
"day": dt.strftime('%Y-%m-%d'),
|
||||||
|
"week": dt.strftime('%Y-%W'), # Week number, Monday is first day
|
||||||
|
"month": dt.strftime('%Y-%m'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_stats_counters(stats_dict: defaultdict, now: datetime):
|
||||||
|
"""Increments the appropriate time-based counters in a stats dictionary."""
|
||||||
|
keys = get_time_keys(now)
|
||||||
|
stats_dict['by_hour'][keys['hour']] += 1
|
||||||
|
stats_dict['by_day'][keys['day']] += 1
|
||||||
|
stats_dict['by_week'][keys['week']] += 1
|
||||||
|
stats_dict['by_month'][keys['month']] += 1
|
||||||
|
|
||||||
|
def generate_time_series_svg(title: str, data: list[tuple[str, int]], y_label: str) -> str:
|
||||||
|
"""Generates a responsive SVG bar chart for time-series data."""
|
||||||
|
if not data:
|
||||||
|
return f"<h3>{html.escape(title)}</h3><p>No data yet.</p>"
|
||||||
|
max_val = max(item[1] for item in data) if data else 1
|
||||||
|
svg_height, svg_width = 250, 600
|
||||||
|
bar_padding = 5
|
||||||
|
bar_width = (svg_width - 50) / len(data) - bar_padding
|
||||||
|
|
||||||
|
bars = ""
|
||||||
|
labels = ""
|
||||||
|
for i, (key, val) in enumerate(data):
|
||||||
|
bar_height = (val / max_val) * (svg_height - 50) if max_val > 0 else 0
|
||||||
|
x = i * (bar_width + bar_padding) + 40
|
||||||
|
y = svg_height - bar_height - 30
|
||||||
|
|
||||||
|
bars += f'<rect x="{x}" y="{y}" width="{bar_width}" height="{bar_height}" fill="#007BFF"><title>{html.escape(key)}: {val}</title></rect>'
|
||||||
|
labels += f'<text x="{x + bar_width / 2}" y="{svg_height - 15}" font-size="11" text-anchor="middle">{html.escape(key)}</text>'
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
<h3>{html.escape(title)}</h3>
|
||||||
|
<div style="border:1px solid #ccc; padding: 10px; border-radius: 5px;">
|
||||||
|
<svg viewBox="0 0 {svg_width} {svg_height}" style="width:100%; height:auto;">
|
||||||
|
<g>{bars}</g>
|
||||||
|
<g>{labels}</g>
|
||||||
|
<line x1="35" y1="10" x2="35" y2="{svg_height - 30}" stroke="#aaa" stroke-width="1" />
|
||||||
|
<line x1="35" y1="{svg_height - 30}" x2="{svg_width - 10}" y2="{svg_height - 30}" stroke="#aaa" stroke-width="1" />
|
||||||
|
<text x="5" y="{svg_height - 30}" font-size="12">0</text>
|
||||||
|
<text x="5" y="20" font-size="12">{max_val}</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def middleware(request, handler):
|
||||||
|
"""Middleware to count all incoming HTTP requests."""
|
||||||
|
# Avoid counting requests to the stats page itself
|
||||||
|
if request.path.startswith('/stats.html'):
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
update_stats_counters(request.app['stats']['http_requests'], datetime.now(timezone.utc))
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
def update_websocket_stats(app):
|
||||||
|
update_stats_counters(app['stats']['websocket_requests'], datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
async def pipe_and_count_websocket(ws_from, ws_to, stats_dict):
|
||||||
|
"""This function proxies WebSocket messages AND counts them."""
|
||||||
|
async for msg in ws_from:
|
||||||
|
# This is the key part for monitoring WebSockets
|
||||||
|
update_stats_counters(stats_dict, datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
if msg.type == WSMsgType.TEXT:
|
||||||
|
await ws_to.send_str(msg.data)
|
||||||
|
elif msg.type == WSMsgType.BINARY:
|
||||||
|
await ws_to.send_bytes(msg.data)
|
||||||
|
elif msg.type in (WSMsgType.CLOSE, WSMsgType.ERROR):
|
||||||
|
await ws_to.close(code=ws_from.close_code)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
async def stats_handler(request: web.Request):
|
||||||
|
"""Handler to display the statistics dashboard."""
|
||||||
|
stats = request.app['stats']
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Helper to prepare data for charts
|
||||||
|
def get_data(source, period, count):
|
||||||
|
data = []
|
||||||
|
for i in range(count - 1, -1, -1):
|
||||||
|
if period == 'hour':
|
||||||
|
dt = now - timedelta(hours=i)
|
||||||
|
key, label = dt.strftime('%Y-%m-%d-%H'), dt.strftime('%H:00')
|
||||||
|
data.append((label, source['by_hour'].get(key, 0)))
|
||||||
|
elif period == 'day':
|
||||||
|
dt = now - timedelta(days=i)
|
||||||
|
key, label = dt.strftime('%Y-%m-%d'), dt.strftime('%a')
|
||||||
|
data.append((label, source['by_day'].get(key, 0)))
|
||||||
|
return data
|
||||||
|
|
||||||
|
http_hourly = get_data(stats['http_requests'], 'hour', 24)
|
||||||
|
ws_hourly = get_data(stats['ws_messages'], 'hour', 24)
|
||||||
|
http_daily = get_data(stats['http_requests'], 'day', 7)
|
||||||
|
ws_daily = get_data(stats['ws_messages'], 'day', 7)
|
||||||
|
|
||||||
|
body = f"""
|
||||||
|
<html><head><title>App Stats</title><meta http-equiv="refresh" content="30"></head>
|
||||||
|
<body>
|
||||||
|
<h2>Application Dashboard</h2>
|
||||||
|
<h3>Last 24 Hours</h3>
|
||||||
|
{generate_time_series_svg("HTTP Requests", http_hourly, "Reqs/Hour")}
|
||||||
|
{generate_time_series_svg("WebSocket Messages", ws_hourly, "Msgs/Hour")}
|
||||||
|
<h3>Last 7 Days</h3>
|
||||||
|
{generate_time_series_svg("HTTP Requests", http_daily, "Reqs/Day")}
|
||||||
|
{generate_time_series_svg("WebSocket Messages", ws_daily, "Msgs/Day")}
|
||||||
|
</body></html>
|
||||||
|
"""
|
||||||
|
return web.Response(text=body, content_type='text/html')
|
||||||
|
|
||||||
+29
-77
@@ -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):
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -72,12 +72,13 @@ function throttle(fn, wait) {
|
|||||||
// --- Scroll: load extra messages, throttled ---
|
// --- Scroll: load extra messages, throttled ---
|
||||||
let isLoadingExtra = false;
|
let isLoadingExtra = false;
|
||||||
async function loadExtra() {
|
async function loadExtra() {
|
||||||
const firstMessage = messagesContainer.children[messagesContainer.children.length - 1];
|
const firstMessage = messagesContainer.lastElementChild;
|
||||||
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
if (isLoadingExtra || !isScrolledPastHalf() || !firstMessage) return;
|
||||||
isLoadingExtra = true;
|
isLoadingExtra = true;
|
||||||
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
const messages = await app.rpc.getMessages(channelUid, 0, firstMessage.dataset.created_at);
|
||||||
if (messages.length) {
|
if (messages.length) {
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
|
messages.reverse();
|
||||||
messages.forEach(msg => {
|
messages.forEach(msg => {
|
||||||
const temp = document.createElement("div");
|
const temp = document.createElement("div");
|
||||||
temp.innerHTML = msg.html;
|
temp.innerHTML = msg.html;
|
||||||
@@ -138,10 +139,16 @@ chatInputField.textarea.focus();
|
|||||||
|
|
||||||
// --- Reply helper ---
|
// --- Reply helper ---
|
||||||
function replyMessage(message) {
|
function replyMessage(message) {
|
||||||
chatInputField.value = "```markdown\n> " + (message || '').split("\n").join("\n> ") + "\n```\n";
|
chatInputField.value = "```markdown\n> " + (message || '').trim().split("\n").join("\n> ") + "\n```\n";
|
||||||
|
chatInputField.textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
chatInputField.focus();
|
chatInputField.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messagesContainer.addEventListener("reply", (e) => {
|
||||||
|
const messageText = e.replyText || e.messageTextTarget.textContent.trim();
|
||||||
|
replyMessage(messageText);
|
||||||
|
})
|
||||||
|
|
||||||
// --- Mention helpers ---
|
// --- Mention helpers ---
|
||||||
function extractMentions(message) {
|
function extractMentions(message) {
|
||||||
return [...new Set(message.match(/@\w+/g) || [])];
|
return [...new Set(message.match(/@\w+/g) || [])];
|
||||||
@@ -215,7 +222,7 @@ document.addEventListener('keydown', function(event) {
|
|||||||
keyTimeout = setTimeout(() => { gPressCount = 0; }, 300);
|
keyTimeout = setTimeout(() => { gPressCount = 0; }, 300);
|
||||||
if (gPressCount === 2) {
|
if (gPressCount === 2) {
|
||||||
gPressCount = 0;
|
gPressCount = 0;
|
||||||
messagesContainer.querySelector(".message:first-child")?.scrollIntoView({ block: "end", inline: "nearest" });
|
messagesContainer.lastElementChild?.scrollIntoView({ block: "end", inline: "nearest" });
|
||||||
loadExtra();
|
loadExtra();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +261,7 @@ function updateLayout(doScrollDown) {
|
|||||||
function isScrolledPastHalf() {
|
function isScrolledPastHalf() {
|
||||||
let scrollTop = messagesContainer.scrollTop;
|
let scrollTop = messagesContainer.scrollTop;
|
||||||
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
let scrollableHeight = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
||||||
return scrollTop < scrollableHeight / 2;
|
return Math.abs(scrollTop) > scrollableHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initial layout update ---
|
// --- Initial layout update ---
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
|
# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.
|
||||||
|
|
||||||
|
from snek.system.stats import update_websocket_stats
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -507,7 +507,9 @@ class RPCView(BaseView):
|
|||||||
raise Exception("Method not found")
|
raise Exception("Method not found")
|
||||||
success = True
|
success = True
|
||||||
try:
|
try:
|
||||||
|
update_websocket_stats(self.app)
|
||||||
result = await method(*args)
|
result = await method(*args)
|
||||||
|
update_websocket_stats(self.app)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
result = {"exception": str(ex), "traceback": traceback.format_exc()}
|
result = {"exception": str(ex), "traceback": traceback.format_exc()}
|
||||||
success = False
|
success = False
|
||||||
|
|||||||
@@ -55,7 +55,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"),
|
||||||
@@ -82,7 +82,6 @@ class WebView(BaseView):
|
|||||||
await self.app.services.notification.mark_as_read(
|
await self.app.services.notification.mark_as_read(
|
||||||
self.session.get("uid"), message["uid"]
|
self.session.get("uid"), message["uid"]
|
||||||
)
|
)
|
||||||
print(messages)
|
|
||||||
name = await channel_member.get_name()
|
name = await channel_member.get_name()
|
||||||
return await self.render_template(
|
return await self.render_template(
|
||||||
"web.html",
|
"web.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user