forked from retoor/snek
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6337350b60 | ||
|
|
986acfac38 | ||
|
|
b27149b5ba | ||
|
|
eb1284060a | ||
|
|
4266ac1f12 | ||
|
|
6b4709d011 | ||
|
|
ef8d3068a8 | ||
|
|
a23c14389b | ||
|
|
6dfd8db0a6 | ||
|
|
abce2e03d1 | ||
|
|
54d7d5b74e | ||
|
|
17bb88050a |
@@ -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,5 +1,5 @@
|
|||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
from snek.system.template import whitelist_attributes
|
from snek.system.template import sanitize_html
|
||||||
import time
|
import time
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
@@ -72,7 +72,7 @@ class ChannelMessageService(BaseService):
|
|||||||
try:
|
try:
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
model["html"] = whitelist_attributes(model["html"])
|
model['html'] = sanitize_html(model['html'])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ class ChannelMessageService(BaseService):
|
|||||||
)
|
)
|
||||||
template = self.app.jinja2_env.get_template("message.html")
|
template = self.app.jinja2_env.get_template("message.html")
|
||||||
model["html"] = template.render(**context)
|
model["html"] = template.render(**context)
|
||||||
model["html"] = whitelist_attributes(model["html"])
|
model['html'] = sanitize_html(model['html'])
|
||||||
return await super().save(model)
|
return await super().save(model)
|
||||||
|
|
||||||
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
async def offset(self, channel_uid, page=0, timestamp=None, page_size=30):
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,17 +57,6 @@ export class ReplyEvent extends Event {
|
|||||||
class MessageElement extends HTMLElement {
|
class MessageElement extends HTMLElement {
|
||||||
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
// static observedAttributes = ['data-uid', 'data-color', 'data-channel_uid', 'data-user_nick', 'data-created_at', 'data-user_uid'];
|
||||||
|
|
||||||
isVisible() {
|
|
||||||
if (!this) return false;
|
|
||||||
const rect = this.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
rect.top >= 0 &&
|
|
||||||
rect.left >= 0 &&
|
|
||||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
||||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
if (this._originalChildren === undefined) {
|
if (this._originalChildren === undefined) {
|
||||||
const { color, user_nick, created_at, user_uid} = this.dataset;
|
const { color, user_nick, created_at, user_uid} = this.dataset;
|
||||||
@@ -104,8 +93,8 @@ class MessageElement extends HTMLElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -177,6 +166,10 @@ class MessageList extends HTMLElement {
|
|||||||
threshold: 0,
|
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) {
|
||||||
@@ -184,10 +177,6 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.endOfMessages = document.createElement('div');
|
|
||||||
this.endOfMessages.classList.add('message-list-bottom');
|
|
||||||
this.prepend(this.endOfMessages);
|
|
||||||
|
|
||||||
this.scrollToBottom(true);
|
this.scrollToBottom(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,13 +232,13 @@ class MessageList extends HTMLElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
isScrolledToBottom() {
|
isScrolledToBottom() {
|
||||||
return this.isElementVisible(this.endOfMessages);
|
return this.visibleSet.has(this.endOfMessages)
|
||||||
}
|
}
|
||||||
scrollToBottom(force = false, behavior= 'instant') {
|
scrollToBottom(force = false, behavior= 'instant') {
|
||||||
if (force || !this.isScrolledToBottom()) {
|
if (force || !this.isScrolledToBottom()) {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.firstElementChild.scrollIntoView({ behavior, block: 'end' });
|
this.endOfMessages.scrollIntoView({ behavior, block: 'end' });
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +291,7 @@ class MessageList extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scrolledToBottom = this.isScrolledToBottom();
|
const scrolledToBottom = this.isScrolledToBottom();
|
||||||
this.prepend(message);
|
this.endOfMessages.after(message);
|
||||||
if (scrolledToBottom) this.scrollToBottom(true);
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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 {
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user