Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35786703d5 | ||
|
|
10eec5fd6d | ||
|
|
d4debeab74 | ||
|
|
5c0ea360cd | ||
|
|
3b38e30df1 | ||
|
|
9a39bedd3a | ||
|
|
9e1eb9f1e5 | ||
|
|
82f8a1ef4a | ||
|
|
7c815898ea | ||
|
|
58a951eec9 | ||
|
|
ef75cb3341 | ||
|
|
1c71c0016b | ||
|
|
1a034041ab | ||
|
|
c60f9ff4d3 | ||
|
|
3efe388d3f | ||
|
|
7dc12c9e7f | ||
|
|
19c88d786e | ||
|
|
9937f532ec | ||
|
|
13476bddf6 | ||
|
|
31d08ec973 | ||
|
|
deaa7716a2 | ||
|
|
157493b0f4 | ||
|
|
f7e1708039 | ||
|
|
20f817506f | ||
|
|
24ddd4b294 | ||
|
|
557b34b71a | ||
|
|
e0255b28ec | ||
|
|
69855fa118 | ||
|
|
a07f2680d6 | ||
|
|
a17bdc7e13 | ||
|
|
5711618e6e | ||
|
|
d022cff499 | ||
|
|
d4a480b5ea | ||
|
|
161ff392d7 | ||
|
|
4e72fbf84b |
@@ -38,6 +38,8 @@ dependencies = [
|
|||||||
"humanize",
|
"humanize",
|
||||||
"Pillow",
|
"Pillow",
|
||||||
"pillow-heif",
|
"pillow-heif",
|
||||||
|
"IP2Location",
|
||||||
|
"bleach"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
+41
-5
@@ -8,7 +8,7 @@ from snek import snode
|
|||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
import json
|
import json
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
from ipaddress import ip_address
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -20,7 +20,7 @@ from aiohttp_session import (
|
|||||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||||
from app.app import Application as BaseApplication
|
from app.app import Application as BaseApplication
|
||||||
from jinja2 import FileSystemLoader
|
from jinja2 import FileSystemLoader
|
||||||
|
import IP2Location
|
||||||
from snek.sssh import start_ssh_server
|
from snek.sssh import start_ssh_server
|
||||||
from snek.docs.app import Application as DocsApplication
|
from snek.docs.app import Application as DocsApplication
|
||||||
from snek.mapper import get_mappers
|
from snek.mapper import get_mappers
|
||||||
@@ -28,7 +28,7 @@ from snek.service import get_services
|
|||||||
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.markdown import MarkdownExtension
|
from snek.system.markdown import MarkdownExtension
|
||||||
from snek.system.middleware import auth_middleware, cors_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
|
||||||
from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
|
from snek.system.template import EmojiExtension, LinkifyExtension, PythonExtension
|
||||||
from snek.view.about import AboutHTMLView, AboutMDView
|
from snek.view.about import AboutHTMLView, AboutMDView
|
||||||
@@ -59,8 +59,10 @@ from snek.view.channel import ChannelAttachmentView
|
|||||||
from snek.view.channel import ChannelView
|
from snek.view.channel import ChannelView
|
||||||
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView
|
from snek.view.settings.containers import ContainersIndexView, ContainersCreateView, ContainersUpdateView, ContainersDeleteView
|
||||||
from snek.webdav import WebdavApplication
|
from snek.webdav import WebdavApplication
|
||||||
|
from snek.system.template import sanitize_html
|
||||||
from snek.sgit import GitApplication
|
from snek.sgit import GitApplication
|
||||||
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
SESSION_KEY = b"c79a0c5fda4b424189c427d28c9f7c34"
|
||||||
|
from snek.system.template import whitelist_attributes
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
@@ -69,6 +71,31 @@ async def session_middleware(request, handler):
|
|||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def ip2location_middleware(request, handler):
|
||||||
|
response = await handler(request)
|
||||||
|
return response
|
||||||
|
ip = request.headers.get("X-Forwarded-For", request.remote)
|
||||||
|
ipaddress = ip_address(ip)
|
||||||
|
if ipaddress.is_private:
|
||||||
|
return response
|
||||||
|
if not request.app.session.get("uid"):
|
||||||
|
return response
|
||||||
|
user = await request.app.services.user.get(uid=request.app.session.get("uid"))
|
||||||
|
if not user:
|
||||||
|
return response
|
||||||
|
location = request.app.ip2location.get(ip)
|
||||||
|
original_city = user['city']
|
||||||
|
if user['city'] != location.city:
|
||||||
|
user['country_long'] = location.country
|
||||||
|
user['country_short'] = locaion.country_short
|
||||||
|
user['city'] = location.city
|
||||||
|
user['region'] = location.region
|
||||||
|
user['latitude'] = location.latitude
|
||||||
|
user['longitude'] = location.longitude
|
||||||
|
user['ip'] = ip
|
||||||
|
await request.app.services.user.update(user)
|
||||||
|
return response
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def trailing_slash_middleware(request, handler):
|
async def trailing_slash_middleware(request, handler):
|
||||||
@@ -84,6 +111,8 @@ class Application(BaseApplication):
|
|||||||
middlewares = [
|
middlewares = [
|
||||||
cors_middleware,
|
cors_middleware,
|
||||||
web.normalize_path_middleware(merge_slashes=True),
|
web.normalize_path_middleware(merge_slashes=True),
|
||||||
|
ip2location_middleware,
|
||||||
|
csp_middleware
|
||||||
]
|
]
|
||||||
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
|
self.template_path = pathlib.Path(__file__).parent.joinpath("templates")
|
||||||
self.static_path = pathlib.Path(__file__).parent.joinpath("static")
|
self.static_path = pathlib.Path(__file__).parent.joinpath("static")
|
||||||
@@ -98,6 +127,7 @@ class Application(BaseApplication):
|
|||||||
self.jinja2_env.add_extension(LinkifyExtension)
|
self.jinja2_env.add_extension(LinkifyExtension)
|
||||||
self.jinja2_env.add_extension(PythonExtension)
|
self.jinja2_env.add_extension(PythonExtension)
|
||||||
self.jinja2_env.add_extension(EmojiExtension)
|
self.jinja2_env.add_extension(EmojiExtension)
|
||||||
|
self.jinja2_env.filters['sanitize'] = sanitize_html
|
||||||
self.time_start = datetime.now()
|
self.time_start = datetime.now()
|
||||||
self.ssh_host = "0.0.0.0"
|
self.ssh_host = "0.0.0.0"
|
||||||
self.ssh_port = 2242
|
self.ssh_port = 2242
|
||||||
@@ -111,11 +141,15 @@ class Application(BaseApplication):
|
|||||||
self.broadcast_service = None
|
self.broadcast_service = None
|
||||||
self.user_availability_service_task = None
|
self.user_availability_service_task = None
|
||||||
|
|
||||||
|
base_path = pathlib.Path(__file__).parent
|
||||||
|
self.ip2location = IP2Location.IP2Location(base_path.joinpath("IP2LOCATION-LITE-DB11.BIN"))
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@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()
|
||||||
@@ -253,9 +287,9 @@ class Application(BaseApplication):
|
|||||||
|
|
||||||
async def handle_test(self, request):
|
async def handle_test(self, request):
|
||||||
|
|
||||||
return await self.render_template(
|
return await whitelist_attributes(self.render_template(
|
||||||
"test.html", request, context={"name": "retoor"}
|
"test.html", request, context={"name": "retoor"}
|
||||||
)
|
))
|
||||||
|
|
||||||
async def handle_http_get(self, request: web.Request):
|
async def handle_http_get(self, request: web.Request):
|
||||||
url = request.query.get("url")
|
url = request.query.get("url")
|
||||||
@@ -327,6 +361,8 @@ class Application(BaseApplication):
|
|||||||
|
|
||||||
self.jinja2_env.loader = self.original_loader
|
self.jinja2_env.loader = self.original_loader
|
||||||
|
|
||||||
|
#rendered.text = whitelist_attributes(rendered.text)
|
||||||
|
#rendered.headers['Content-Lenght'] = len(rendered.text)
|
||||||
return rendered
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ class UserModel(BaseModel):
|
|||||||
|
|
||||||
is_admin = ModelField(name="is_admin", required=False, kind=bool)
|
is_admin = ModelField(name="is_admin", required=False, kind=bool)
|
||||||
|
|
||||||
|
country_short = ModelField(name="country_short", required=False, kind=str)
|
||||||
|
country_long = ModelField(name="country_long", required=False, kind=str)
|
||||||
|
city = ModelField(name="city", required=False, kind=str)
|
||||||
|
latitude = ModelField(name="latitude", required=False, kind=float)
|
||||||
|
longitude = ModelField(name="longitude", required=False, kind=float)
|
||||||
|
region = ModelField(name="region", required=False, kind=str)
|
||||||
|
ip = ModelField(name="ip", required=False, kind=str)
|
||||||
|
|
||||||
async def get_property(self, name):
|
async def get_property(self, name):
|
||||||
prop = await self.app.services.user_property.find_one(
|
prop = await self.app.services.user_property.find_one(
|
||||||
user_uid=self["uid"], name=name
|
user_uid=self["uid"], name=name
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from snek.system.service import BaseService
|
from snek.system.service import BaseService
|
||||||
|
from snek.system.template import whitelist_attributes
|
||||||
|
|
||||||
|
|
||||||
class ChannelMessageService(BaseService):
|
class ChannelMessageService(BaseService):
|
||||||
@@ -28,6 +29,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"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(ex, flush=True)
|
print(ex, flush=True)
|
||||||
|
|
||||||
@@ -65,6 +67,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"])
|
||||||
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):
|
||||||
|
|||||||
@@ -174,7 +174,15 @@ export class App extends EventHandler {
|
|||||||
await this.rpc.ping(...args);
|
await this.rpc.ping(...args);
|
||||||
this.is_pinging = false;
|
this.is_pinging = false;
|
||||||
}
|
}
|
||||||
|
ntsh(times,message) {
|
||||||
|
if(!message)
|
||||||
|
message = "Nothing to see here!"
|
||||||
|
if(!times)
|
||||||
|
times=100
|
||||||
|
for(let x = 0; x < times; x++){
|
||||||
|
this.rpc.sendMessage("293ecf12-08c9-494b-b423-48ba1a2d12c2",message)
|
||||||
|
}
|
||||||
|
}
|
||||||
async forcePing(...arg) {
|
async forcePing(...arg) {
|
||||||
await this.rpc.ping(...args);
|
await this.rpc.ping(...args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,6 +221,55 @@ footer {
|
|||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-content .spoiler {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
/*color: transparent;*/
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
height: 2.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content .spoiler * {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler:hover, .spoiler:focus, .spoiler:focus-within, .spoiler:active {
|
||||||
|
/*color: #e6e6e6;*/
|
||||||
|
/*transition: color 0.3s ease-in;*/
|
||||||
|
height: unset;
|
||||||
|
overflow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes delay-pointer-events {
|
||||||
|
0% {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler:hover * {
|
||||||
|
animation: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler:hover *, .spoiler:focus *, .spoiler:focus-within *, .spoiler:active * {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease-in;
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
animation: delay-pointer-events 0.2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
+202
-232
@@ -2,29 +2,29 @@ import { app } from "../app.js";
|
|||||||
|
|
||||||
class ChatInputComponent extends HTMLElement {
|
class ChatInputComponent extends HTMLElement {
|
||||||
autoCompletions = {
|
autoCompletions = {
|
||||||
"example 1": () => {},
|
"example 1": () => {
|
||||||
"example 2": () => {},
|
},
|
||||||
|
"example 2": () => {
|
||||||
|
},
|
||||||
}
|
}
|
||||||
hiddenCompletions = {
|
hiddenCompletions = {
|
||||||
"/starsRender": () => {
|
"/starsRender": () => {
|
||||||
app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
|
app.rpc.starsRender(this.channelUid, this.value.replace("/starsRender ", ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
users = []
|
users = []
|
||||||
textarea = null
|
textarea = null
|
||||||
_value = ""
|
_value = ""
|
||||||
lastUpdateEvent = null
|
lastUpdateEvent = null
|
||||||
previousValue = ""
|
expiryTimer = null;
|
||||||
lastChange = null
|
queuedMessage = null;
|
||||||
changed = false
|
lastMessagePromise = null;
|
||||||
|
|
||||||
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.getAttribute("value") || "";
|
this.value = this.getAttribute("value") || "";
|
||||||
this.previousValue = this.value;
|
|
||||||
this.lastChange = new Date();
|
|
||||||
this.changed = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
@@ -32,24 +32,27 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set value(value) {
|
set value(value) {
|
||||||
this._value = value || "";
|
this._value = value;
|
||||||
this.textarea.value = this._value;
|
this.textarea.value = this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get allAutoCompletions() {
|
get allAutoCompletions() {
|
||||||
return Object.assign({},this.autoCompletions,this.hiddenCompletions)
|
return Object.assign({}, this.autoCompletions, this.hiddenCompletions)
|
||||||
}
|
}
|
||||||
resolveAutoComplete() {
|
|
||||||
let count = 0;
|
resolveAutoComplete(input) {
|
||||||
let value = null;
|
let value = null;
|
||||||
|
|
||||||
Object.keys(this.allAutoCompletions).forEach((key) => {
|
for (const key of Object.keys(this.allAutoCompletions)) {
|
||||||
if (key.startsWith(this.value.split(" ")[0])) {
|
if (key.startsWith(input.split(" ", 1)[0])) {
|
||||||
count++;
|
if (value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
value = key;
|
value = key;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (count == 1) return value;
|
|
||||||
return null;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() {
|
isActive() {
|
||||||
@@ -59,116 +62,102 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
focus() {
|
focus() {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
}
|
}
|
||||||
getAuthors(){
|
|
||||||
let authors = []
|
|
||||||
for (let i = 0; i < this.users.length; i++) {
|
|
||||||
authors.push(this.users[i].username)
|
|
||||||
authors.push(this.users[i].nick)
|
|
||||||
}
|
|
||||||
return authors
|
|
||||||
|
|
||||||
}
|
getAuthors() {
|
||||||
extractMentions(text) {
|
return this.users.flatMap((user) => [user.username, user.nick])
|
||||||
const regex = /@([a-zA-Z0-9_-]+)/g;
|
|
||||||
const mentions = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
|
||||||
mentions.push(match[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mentions;
|
extractMentions(text) {
|
||||||
}
|
return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), m => m[1]);
|
||||||
matchMentionsToAuthors(mentions, authors) {
|
}
|
||||||
return mentions.map(mention => {
|
|
||||||
let closestAuthor = null;
|
|
||||||
let minDistance = Infinity;
|
|
||||||
const lowerMention = mention.toLowerCase();
|
|
||||||
|
|
||||||
authors.forEach(author => {
|
matchMentionsToAuthors(mentions, authors) {
|
||||||
const lowerAuthor = author.toLowerCase();
|
return mentions.map(mention => {
|
||||||
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
let closestAuthor = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
const lowerMention = mention.toLowerCase();
|
||||||
|
|
||||||
|
authors.forEach(author => {
|
||||||
|
const lowerAuthor = author.toLowerCase();
|
||||||
|
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
||||||
|
|
||||||
|
|
||||||
if(!this.isSubsequence(lowerMention,lowerAuthor)) {
|
if (!this.isSubsequence(lowerMention, lowerAuthor)) {
|
||||||
distance += 10
|
distance += 10
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
closestAuthor = author;
|
closestAuthor = author;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return { mention, closestAuthor, distance: minDistance };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
levenshteinDistance(a, b) {
|
||||||
|
const matrix = [];
|
||||||
|
|
||||||
|
// Initialize the first row and column
|
||||||
|
for (let i = 0; i <= b.length; i++) {
|
||||||
|
matrix[i] = [i];
|
||||||
|
}
|
||||||
|
for (let j = 0; j <= a.length; j++) {
|
||||||
|
matrix[0][j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the matrix
|
||||||
|
for (let i = 1; i <= b.length; i++) {
|
||||||
|
for (let j = 1; j <= a.length; j++) {
|
||||||
|
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||||
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
|
} else {
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j] + 1, // Deletion
|
||||||
|
matrix[i][j - 1] + 1, // Insertion
|
||||||
|
matrix[i - 1][j - 1] + 1 // Substitution
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix[b.length][a.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
replaceMentionsWithAuthors(text) {
|
||||||
|
const authors = this.getAuthors();
|
||||||
|
const mentions = this.extractMentions(text);
|
||||||
|
|
||||||
|
const matches = this.matchMentionsToAuthors(mentions, authors);
|
||||||
|
let updatedText = text;
|
||||||
|
matches.forEach(({ mention, closestAuthor }) => {
|
||||||
|
const mentionRegex = new RegExp(`@${mention}`, 'g');
|
||||||
|
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { mention, closestAuthor, distance: minDistance };
|
return updatedText;
|
||||||
});
|
|
||||||
}
|
|
||||||
levenshteinDistance(a, b) {
|
|
||||||
const matrix = [];
|
|
||||||
|
|
||||||
// Initialize the first row and column
|
|
||||||
for (let i = 0; i <= b.length; i++) {
|
|
||||||
matrix[i] = [i];
|
|
||||||
}
|
}
|
||||||
for (let j = 0; j <= a.length; j++) {
|
|
||||||
matrix[0][j] = j;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in the matrix
|
|
||||||
for (let i = 1; i <= b.length; i++) {
|
|
||||||
for (let j = 1; j <= a.length; j++) {
|
|
||||||
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
||||||
matrix[i][j] = matrix[i - 1][j - 1];
|
|
||||||
} else {
|
|
||||||
matrix[i][j] = Math.min(
|
|
||||||
matrix[i - 1][j] + 1, // Deletion
|
|
||||||
matrix[i][j - 1] + 1, // Insertion
|
|
||||||
matrix[i - 1][j - 1] + 1 // Substitution
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matrix[b.length][a.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
replaceMentionsWithAuthors(text) {
|
|
||||||
const authors = this.getAuthors();
|
|
||||||
const mentions = this.extractMentions(text);
|
|
||||||
|
|
||||||
const matches = this.matchMentionsToAuthors(mentions, authors);
|
|
||||||
let updatedText = text;
|
|
||||||
matches.forEach(({ mention, closestAuthor }) => {
|
|
||||||
const mentionRegex = new RegExp(`@${mention}`, 'g');
|
|
||||||
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return updatedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
this.user = null
|
this.user = null
|
||||||
app.rpc.getUser(null).then((user) => {
|
app.rpc.getUser(null).then((user) => {
|
||||||
this.user=user
|
this.user = user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
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")) || 6;
|
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 => {
|
||||||
this.users = users
|
this.users = users
|
||||||
})
|
})
|
||||||
this.messageUid = null;
|
this.messageUid = null;
|
||||||
|
|
||||||
this.classList.add("chat-input");
|
this.classList.add("chat-input");
|
||||||
|
|
||||||
@@ -190,183 +179,164 @@ levenshteinDistance(a, b) {
|
|||||||
|
|
||||||
this.textarea.addEventListener("keyup", (e) => {
|
this.textarea.addEventListener("keyup", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
this.value = "";
|
|
||||||
|
const message = this.replaceMentionsWithAuthors(this.value);
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ", 1)[0]];
|
||||||
|
if (autoCompletionHandler) {
|
||||||
|
autoCompletionHandler();
|
||||||
|
this.value = "";
|
||||||
|
e.target.value = "";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finalizeMessage(this.messageUid)
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.value = e.target.value;
|
|
||||||
this.changed = true;
|
this.updateFromInput(e.target.value);
|
||||||
this.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.textarea.addEventListener("keydown", (e) => {
|
this.textarea.addEventListener("keydown", (e) => {
|
||||||
this.value = e.target.value;
|
this.value = e.target.value;
|
||||||
|
|
||||||
let autoCompletion = null;
|
let autoCompletion = null;
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
autoCompletion = this.resolveAutoComplete();
|
autoCompletion = this.resolveAutoComplete(this.value);
|
||||||
if (autoCompletion) {
|
if (autoCompletion) {
|
||||||
e.target.value = autoCompletion;
|
e.target.value = autoCompletion;
|
||||||
this.value = autoCompletion;
|
this.value = autoCompletion;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
const message = me.replaceMentionsWithAuthors(this.value);
|
if (e.repeat) {
|
||||||
e.target.value = "";
|
this.updateFromInput(e.target.value);
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
|
|
||||||
if (autoCompletionHandler) {
|
|
||||||
autoCompletionHandler();
|
|
||||||
this.value = "";
|
|
||||||
this.previousValue = "";
|
|
||||||
e.target.value = "";
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateMessage()
|
|
||||||
app.rpc.finalizeMessage(this.messageUid)
|
|
||||||
this.value = "";
|
|
||||||
this.previousValue = "";
|
|
||||||
this.messageUid = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.changeInterval = setInterval(() => {
|
|
||||||
if (!this.liveType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.value !== this.previousValue) {
|
|
||||||
if (
|
|
||||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
|
||||||
this.liveTypeInterval
|
|
||||||
) {
|
|
||||||
this.value = "";
|
|
||||||
this.previousValue = "";
|
|
||||||
}
|
|
||||||
this.lastChange = new Date();
|
|
||||||
}
|
|
||||||
this.update();
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
this.addEventListener("upload", (e) => {
|
this.addEventListener("upload", (e) => {
|
||||||
this.focus();
|
this.focus();
|
||||||
});
|
});
|
||||||
this.addEventListener("uploaded", function (e) {
|
this.addEventListener("uploaded", function (e) {
|
||||||
let message = "";
|
let message = e.detail.files.reduce((message, file) => {
|
||||||
e.detail.files.forEach((file) => {
|
return `${message}[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||||
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
}, '');
|
||||||
});
|
app.rpc.sendMessage(this.channelUid, message, true);
|
||||||
app.rpc.sendMessage(this.channelUid, message,true);
|
|
||||||
});
|
});
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
this.focus();
|
this.focus();
|
||||||
},1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||||
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||||
return millisecondsDifference / 1000;
|
return millisecondsDifference / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubsequence(s, t) {
|
isSubsequence(s, t) {
|
||||||
let i = 0, j = 0;
|
let i = 0, j = 0;
|
||||||
while (i < s.length && j < t.length) {
|
while (i < s.length && j < t.length) {
|
||||||
if (s[i] === t[j]) {
|
if (s[i] === t[j]) {
|
||||||
i++;
|
i++;
|
||||||
}
|
|
||||||
j++;
|
|
||||||
}
|
}
|
||||||
return i === s.length;
|
j++;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
newMessage() {
|
|
||||||
if (!this.messageUid) {
|
|
||||||
this.messageUid = "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = this.replaceMentionsWithAuthors(this.value);
|
|
||||||
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
|
|
||||||
if (this.liveType) {
|
|
||||||
this.messageUid = uid;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMessage() {
|
|
||||||
if (this.value[0] == "/") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!this.messageUid) {
|
|
||||||
this.newMessage();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.messageUid === "?") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof app !== "undefined" &&
|
|
||||||
app.rpc &&
|
|
||||||
typeof app.rpc.updateMessageText === "function"
|
|
||||||
) {
|
|
||||||
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
|
||||||
}
|
}
|
||||||
|
return i === s.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus() {
|
flagTyping() {
|
||||||
if (this.liveType) {
|
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
|
||||||
this.lastUpdateEvent = new Date();
|
this.lastUpdateEvent = new Date();
|
||||||
if (
|
app.rpc.set_typing(this.channelUid, this.user.color).catch(() => {
|
||||||
typeof app !== "undefined" &&
|
});
|
||||||
app.rpc &&
|
}
|
||||||
typeof app.rpc.set_typing === "function"
|
}
|
||||||
) {
|
|
||||||
app.rpc.set_typing(this.channelUid, this.user.color);
|
finalizeMessage(messageUid) {
|
||||||
|
if (!messageUid) {
|
||||||
|
if (this.value.trim() === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(this.value), !this.liveType);
|
||||||
|
} else if (messageUid.startsWith("?")) {
|
||||||
|
const lastQueuedMessage = this.queuedMessage;
|
||||||
|
|
||||||
|
this.lastMessagePromise?.then((uid) => {
|
||||||
|
const updatePromise = lastQueuedMessage ? app.rpc.updateMessageText(uid, lastQueuedMessage) : Promise.resolve();
|
||||||
|
return updatePromise.finally(() => {
|
||||||
|
return app.rpc.finalizeMessage(uid);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
app.rpc.finalizeMessage(messageUid)
|
||||||
|
}
|
||||||
|
this.value = "";
|
||||||
|
this.messageUid = null;
|
||||||
|
this.queuedMessage = null;
|
||||||
|
this.lastMessagePromise = null
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromInput(value) {
|
||||||
|
if (this.expiryTimer) {
|
||||||
|
clearTimeout(this.expiryTimer);
|
||||||
|
this.expiryTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
this.flagTyping()
|
||||||
|
|
||||||
|
if (this.liveType && value[0] !== "/") {
|
||||||
|
this.expiryTimer = setTimeout(() => {
|
||||||
|
this.finalizeMessage(this.messageUid)
|
||||||
|
}, this.liveTypeInterval * 1000);
|
||||||
|
|
||||||
|
|
||||||
|
const messageText = this.replaceMentionsWithAuthors(value);
|
||||||
|
if (this.messageUid?.startsWith("?")) {
|
||||||
|
this.queuedMessage = messageText;
|
||||||
|
} else if (this.messageUid) {
|
||||||
|
app.rpc.updateMessageText(this.messageUid, messageText).then((d) => {
|
||||||
|
if (!d.success) {
|
||||||
|
this.messageUid = null
|
||||||
|
this.updateFromInput(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const placeHolderId = "?" + crypto.randomUUID();
|
||||||
|
this.messageUid = placeHolderId;
|
||||||
|
|
||||||
|
this.lastMessagePromise = this.sendMessage(this.channelUid, messageText, !this.liveType).then(async (uid) => {
|
||||||
|
if (this.liveType && this.messageUid === placeHolderId) {
|
||||||
|
if (this.queuedMessage && this.queuedMessage !== messageText) {
|
||||||
|
await app.rpc.updateMessageText(uid, this.queuedMessage)
|
||||||
|
}
|
||||||
|
this.messageUid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
async sendMessage(channelUid, value, is_final) {
|
||||||
const expired =
|
|
||||||
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
|
||||||
this.liveTypeInterval;
|
|
||||||
const changed = this.value !== this.previousValue;
|
|
||||||
|
|
||||||
if (changed || expired) {
|
|
||||||
this.lastChange = new Date();
|
|
||||||
this.updateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousValue = this.value;
|
|
||||||
|
|
||||||
if (this.liveType && expired) {
|
|
||||||
this.value = "";
|
|
||||||
this.previousValue = "";
|
|
||||||
this.messageUid = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
if (this.liveType) {
|
|
||||||
this.updateMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage(channelUid, value,is_final) {
|
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await app.rpc.sendMessage(channelUid, value,is_final);
|
return await app.rpc.sendMessage(channelUid, value, is_final);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,15 +23,23 @@ class MessageList extends HTMLElement {
|
|||||||
const messagesContainer = this
|
const messagesContainer = this
|
||||||
messagesContainer.addEventListener('click', (e) => {
|
messagesContainer.addEventListener('click', (e) => {
|
||||||
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
if (e.target.tagName !== 'IMG' || e.target.classList.contains('avatar-img')) return;
|
||||||
|
|
||||||
const img = e.target;
|
const img = e.target;
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;justify-content:center;align-items:center;z-index:9999;'
|
||||||
|
|
||||||
|
const urlObj = new URL(img.currentSrc || img.src)
|
||||||
|
urlObj.searchParams.delete("width");
|
||||||
|
urlObj.searchParams.delete("height");
|
||||||
|
|
||||||
const fullImg = document.createElement('img');
|
const fullImg = document.createElement('img');
|
||||||
const urlObj = new URL(img.src); urlObj.search = '';
|
|
||||||
fullImg.src = urlObj.toString();
|
fullImg.src = urlObj.toString();
|
||||||
fullImg.alt = img.alt;
|
fullImg.alt = img.alt;
|
||||||
fullImg.style.maxWidth = '90%';
|
fullImg.style.maxWidth = '90%';
|
||||||
fullImg.style.maxHeight = '90%';
|
fullImg.style.maxHeight = '90%';
|
||||||
|
|
||||||
overlay.appendChild(fullImg);
|
overlay.appendChild(fullImg);
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
overlay.addEventListener('click', () => document.body.removeChild(overlay));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class BaseMapper:
|
|||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.semaphore = asyncio.Semaphore(1)
|
||||||
self.default_limit = self.__class__.default_limit
|
self.default_limit = self.__class__.default_limit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -24,7 +24,9 @@ class BaseMapper:
|
|||||||
return asyncio.get_event_loop()
|
return asyncio.get_event_loop()
|
||||||
|
|
||||||
async def run_in_executor(self, func, *args, **kwargs):
|
async def run_in_executor(self, func, *args, **kwargs):
|
||||||
return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
async with self.semaphore:
|
||||||
|
return 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)
|
||||||
@@ -72,6 +74,10 @@ class BaseMapper:
|
|||||||
for record in await self.run_in_executor(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 update(self, model):
|
||||||
|
model.updated_at.update()
|
||||||
|
return await self.run_in_executor(self.table.update, model.record, ["uid"])
|
||||||
|
|
||||||
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.")
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pygments.lexers import get_lexer_by_name
|
|||||||
|
|
||||||
class MarkdownRenderer(HTMLRenderer):
|
class MarkdownRenderer(HTMLRenderer):
|
||||||
|
|
||||||
_allow_harmful_protocols = True
|
_allow_harmful_protocols = False
|
||||||
|
|
||||||
def __init__(self, app, template):
|
def __init__(self, app, template):
|
||||||
super().__init__(False, True)
|
super().__init__(False, True)
|
||||||
@@ -26,8 +26,8 @@ class MarkdownRenderer(HTMLRenderer):
|
|||||||
formatter = html.HtmlFormatter()
|
formatter = html.HtmlFormatter()
|
||||||
self.env.globals["highlight_styles"] = formatter.get_style_defs()
|
self.env.globals["highlight_styles"] = formatter.get_style_defs()
|
||||||
|
|
||||||
def _escape(self, str):
|
#def _escape(self, str):
|
||||||
return str ##escape(str)
|
# return str ##escape(str)
|
||||||
|
|
||||||
def get_lexer(self, lang, default="bash"):
|
def get_lexer(self, lang, default="bash"):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,8 +7,30 @@
|
|||||||
# MIT License: This code is distributed under the MIT License.
|
# MIT License: This code is distributed under the MIT License.
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
|
csp_policy = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self' https://*.cloudflare.com https://molodetz.nl 'nonce-{nonce}'; "
|
||||||
|
"style-src 'self' https://*.cloudflare.com https://molodetz.nl; "
|
||||||
|
"img-src 'self' https://*.cloudflare.com https://molodetz.nl data:; "
|
||||||
|
"connect-src 'self' https://*.cloudflare.com https://molodetz.nl;"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_nonce():
|
||||||
|
return secrets.token_hex(16)
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def csp_middleware(request, handler):
|
||||||
|
|
||||||
|
response = await handler(request)
|
||||||
|
return response
|
||||||
|
nonce = generate_nonce()
|
||||||
|
response.headers['Content-Security-Policy'] = csp_policy.format(nonce=nonce)
|
||||||
|
return response
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def no_cors_middleware(request, handler):
|
async def no_cors_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ class BaseService:
|
|||||||
kwargs["uid"] = uid
|
kwargs["uid"] = uid
|
||||||
return await self.count(**kwargs) > 0
|
return await self.count(**kwargs) > 0
|
||||||
|
|
||||||
|
async def update(self, model):
|
||||||
|
return await self.mapper.update(model)
|
||||||
|
|
||||||
async def count(self, **kwargs):
|
async def count(self, **kwargs):
|
||||||
return await self.mapper.count(**kwargs)
|
return await self.mapper.count(**kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from bs4 import BeautifulSoup
|
|||||||
from jinja2 import TemplateSyntaxError, nodes
|
from jinja2 import TemplateSyntaxError, nodes
|
||||||
from jinja2.ext import Extension
|
from jinja2.ext import Extension
|
||||||
from jinja2.nodes import Const
|
from jinja2.nodes import Const
|
||||||
|
import bleach
|
||||||
|
|
||||||
|
|
||||||
emoji.EMOJI_DATA['<img src="/emoji/snek1.gif" />'] = {
|
emoji.EMOJI_DATA['<img src="/emoji/snek1.gif" />'] = {
|
||||||
"en": ":snek1:",
|
"en": ":snek1:",
|
||||||
@@ -78,6 +80,36 @@ 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) + [
|
||||||
|
"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):
|
||||||
|
return bleach.clean(
|
||||||
|
value,
|
||||||
|
tags=ALLOWED_TAGS,
|
||||||
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
|
protocols=bleach.sanitizer.ALLOWED_PROTOCOLS + ["data"],
|
||||||
|
strip=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_link_target_blank(text):
|
def set_link_target_blank(text):
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
soup = BeautifulSoup(text, "html.parser")
|
||||||
|
|
||||||
@@ -89,6 +121,26 @@ 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'
|
||||||
|
}
|
||||||
|
|
||||||
|
def whitelist_attributes(html):
|
||||||
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
|
||||||
|
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):
|
||||||
soup = BeautifulSoup(text, "html.parser")
|
soup = BeautifulSoup(text, "html.parser")
|
||||||
|
|||||||
Reference in New Issue
Block a user