Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
6350a7851c | |||
5b3e12ec4b | |||
82dc018676 | |||
![]() |
40d11dc66a | ||
![]() |
b9a142ba48 | ||
afeb59d0c1 | |||
![]() |
56ef299c5d | ||
fd5d1c3401 | |||
4a6bba7734 | |||
c32d3509b3 | |||
213136b92e | |||
0dc75452d6 | |||
9d1f6831df | |||
0341e2f2d8 | |||
c4739ff7d3 | |||
cac691e9ff | |||
0e12f7a75c | |||
3872dafaf1 | |||
6a905c1948 | |||
bf576bc0e3 | |||
2c182ad48d | |||
a4d29b9d6f | |||
52538d0181 | |||
75f12c1971 | |||
a41da84e3f |
@ -5,6 +5,7 @@ import ssl
|
|||||||
import uuid
|
import uuid
|
||||||
import signal
|
import signal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from snek import snode
|
from snek import snode
|
||||||
from snek.view.threads import ThreadsView
|
from snek.view.threads import ThreadsView
|
||||||
@ -231,6 +232,7 @@ class Application(BaseApplication):
|
|||||||
print(ex)
|
print(ex)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def prepare_database(self, app):
|
async def prepare_database(self, app):
|
||||||
self.db.query("PRAGMA journal_mode=WAL")
|
self.db.query("PRAGMA journal_mode=WAL")
|
||||||
self.db.query("PRAGMA syncnorm=off")
|
self.db.query("PRAGMA syncnorm=off")
|
||||||
@ -245,7 +247,7 @@ class Application(BaseApplication):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
await app.services.drive.prepare_all()
|
await self.services.drive.prepare_all()
|
||||||
self.loop.create_task(self.task_runner())
|
self.loop.create_task(self.task_runner())
|
||||||
|
|
||||||
def setup_router(self):
|
def setup_router(self):
|
||||||
@ -407,6 +409,11 @@ class Application(BaseApplication):
|
|||||||
request.session.get("uid")
|
request.session.get("uid")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
context["nonce"] = request['csp_nonce']
|
||||||
|
except:
|
||||||
|
context['nonce'] = '?'
|
||||||
|
|
||||||
rendered = await super().render_template(template, request, context)
|
rendered = await super().render_template(template, request, context)
|
||||||
|
|
||||||
self.jinja2_env.loader = self.original_loader
|
self.jinja2_env.loader = self.original_loader
|
||||||
@ -453,6 +460,27 @@ class Application(BaseApplication):
|
|||||||
template_paths.append(self.template_path)
|
template_paths.append(self.template_path)
|
||||||
return FileSystemLoader(template_paths)
|
return FileSystemLoader(template_paths)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def no_save(self):
|
||||||
|
stats = {
|
||||||
|
'count': 0
|
||||||
|
}
|
||||||
|
async def patched_save(*args, **kwargs):
|
||||||
|
await self.cache.set(args[0]["uid"], args[0])
|
||||||
|
stats['count'] = stats['count'] + 1
|
||||||
|
print(f"save is ignored {stats['count']} times")
|
||||||
|
return args[0]
|
||||||
|
save_original = self.services.channel_message.mapper.save
|
||||||
|
self.services.channel_message.mapper.save = patched_save
|
||||||
|
raised_exception = None
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except Exception as ex:
|
||||||
|
raised_exception = ex
|
||||||
|
finally:
|
||||||
|
self.services.channel_message.mapper.save = save_original
|
||||||
|
if raised_exception:
|
||||||
|
raise raised_exception
|
||||||
|
|
||||||
app = Application(db_path="sqlite:///snek.db")
|
app = Application(db_path="sqlite:///snek.db")
|
||||||
|
|
||||||
|
@ -11,11 +11,15 @@ class ChannelModel(BaseModel):
|
|||||||
is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True)
|
is_listed = ModelField(name="is_listed", required=True, kind=bool, value=True)
|
||||||
index = ModelField(name="index", required=True, kind=int, value=1000)
|
index = ModelField(name="index", required=True, kind=int, value=1000)
|
||||||
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)
|
||||||
|
|
||||||
async def get_last_message(self) -> ChannelMessageModel:
|
async def get_last_message(self) -> ChannelMessageModel:
|
||||||
|
history_start_filter = ""
|
||||||
|
if self["history_start"]:
|
||||||
|
history_start_filter = f" AND created_at > '{self['history_start']}' "
|
||||||
try:
|
try:
|
||||||
async for model in self.app.services.channel_message.query(
|
async for model in self.app.services.channel_message.query(
|
||||||
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT 1",
|
"SELECT uid FROM channel_message WHERE channel_uid=:channel_uid" + history_start_filter + " ORDER BY created_at DESC LIMIT 1",
|
||||||
{"channel_uid": self["uid"]},
|
{"channel_uid": self["uid"]},
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -113,6 +113,12 @@ class ChannelService(BaseService):
|
|||||||
channel = await self.get(uid=channel_member["channel_uid"])
|
channel = await self.get(uid=channel_member["channel_uid"])
|
||||||
yield channel
|
yield channel
|
||||||
|
|
||||||
|
async def clear(self, channel_uid):
|
||||||
|
model = await self.get(uid=channel_uid)
|
||||||
|
model['history_from'] = datetime.now()
|
||||||
|
await self.save(model)
|
||||||
|
|
||||||
|
|
||||||
async def ensure_public_channel(self, created_by_uid):
|
async def ensure_public_channel(self, created_by_uid):
|
||||||
model = await self.get(is_listed=True, tag="public")
|
model = await self.get(is_listed=True, tag="public")
|
||||||
is_moderator = False
|
is_moderator = False
|
||||||
|
@ -6,8 +6,10 @@ class ChannelMessageService(BaseService):
|
|||||||
mapper_name = "channel_message"
|
mapper_name = "channel_message"
|
||||||
|
|
||||||
async def maintenance(self):
|
async def maintenance(self):
|
||||||
|
args = {}
|
||||||
async for message in self.find():
|
async for message in self.find():
|
||||||
updated_at = message["updated_at"]
|
updated_at = message["updated_at"]
|
||||||
|
message["is_final"] = True
|
||||||
html = message["html"]
|
html = message["html"]
|
||||||
await self.save(message)
|
await self.save(message)
|
||||||
|
|
||||||
@ -21,6 +23,19 @@ class ChannelMessageService(BaseService):
|
|||||||
if html != message["html"]:
|
if html != message["html"]:
|
||||||
print("Reredefined message", message["uid"])
|
print("Reredefined message", message["uid"])
|
||||||
|
|
||||||
|
while True:
|
||||||
|
changed = 0
|
||||||
|
async for message in self.find(is_final=False):
|
||||||
|
message["is_final"] = True
|
||||||
|
await self.save(message)
|
||||||
|
changed += 1
|
||||||
|
async for message in self.find(is_final=None):
|
||||||
|
message["is_final"] = False
|
||||||
|
await self.save(message)
|
||||||
|
changed += 1
|
||||||
|
if not changed:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
async def create(self, channel_uid, user_uid, message, is_final=True):
|
async def create(self, channel_uid, user_uid, message, is_final=True):
|
||||||
model = await self.new()
|
model = await self.new()
|
||||||
@ -87,13 +102,20 @@ class ChannelMessageService(BaseService):
|
|||||||
model["html"] = whitelist_attributes(model["html"])
|
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):
|
||||||
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
|
if not channel:
|
||||||
|
return []
|
||||||
|
history_start_filter = ""
|
||||||
|
if channel["history_start"]:
|
||||||
|
history_start_filter = f" AND created_at > '{channel['history_start']}'"
|
||||||
results = []
|
results = []
|
||||||
offset = page * page_size
|
offset = page * page_size
|
||||||
try:
|
try:
|
||||||
if timestamp:
|
if timestamp:
|
||||||
async for model in self.query(
|
async for model in self.query(
|
||||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid AND created_at < :timestamp {history_start_filter} ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||||
{
|
{
|
||||||
"channel_uid": channel_uid,
|
"channel_uid": channel_uid,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
@ -104,7 +126,7 @@ class ChannelMessageService(BaseService):
|
|||||||
results.append(model)
|
results.append(model)
|
||||||
elif page > 0:
|
elif page > 0:
|
||||||
async for model in self.query(
|
async for model in self.query(
|
||||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp ORDER BY created_at DESC LIMIT :page_size",
|
f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid WHERE created_at < :timestamp {history_start_filter} ORDER BY created_at DESC LIMIT :page_size",
|
||||||
{
|
{
|
||||||
"channel_uid": channel_uid,
|
"channel_uid": channel_uid,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
@ -115,7 +137,7 @@ class ChannelMessageService(BaseService):
|
|||||||
results.append(model)
|
results.append(model)
|
||||||
else:
|
else:
|
||||||
async for model in self.query(
|
async for model in self.query(
|
||||||
"SELECT * FROM channel_message WHERE channel_uid=:channel_uid ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
f"SELECT * FROM channel_message WHERE channel_uid=:channel_uid {history_start_filter} ORDER BY created_at DESC LIMIT :page_size OFFSET :offset",
|
||||||
{
|
{
|
||||||
"channel_uid": channel_uid,
|
"channel_uid": channel_uid,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
@ -124,7 +146,7 @@ class ChannelMessageService(BaseService):
|
|||||||
):
|
):
|
||||||
results.append(model)
|
results.append(model)
|
||||||
|
|
||||||
except:
|
except Exception as ex:
|
||||||
pass
|
print(ex)
|
||||||
results.sort(key=lambda x: x["created_at"])
|
results.sort(key=lambda x: x["created_at"])
|
||||||
return results
|
return results
|
||||||
|
@ -36,6 +36,18 @@ class ChatService(BaseService):
|
|||||||
channel = await self.services.channel.get(uid=channel_uid)
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
if not channel:
|
if not channel:
|
||||||
raise Exception("Channel not found.")
|
raise Exception("Channel not found.")
|
||||||
|
channel_message = await self.services.channel_message.get(
|
||||||
|
channel_uid=channel_uid,user_uid=user_uid, is_final=False
|
||||||
|
)
|
||||||
|
if channel_message:
|
||||||
|
channel_message["message"] = message
|
||||||
|
channel_message["is_final"] = is_final
|
||||||
|
if not channel_message["is_final"]:
|
||||||
|
async with self.app.no_save():
|
||||||
|
await self.services.channel_message.save(channel_message)
|
||||||
|
else:
|
||||||
|
await self.services.channel_message.save(channel_message)
|
||||||
|
else:
|
||||||
channel_message = await self.services.channel_message.create(
|
channel_message = await self.services.channel_message.create(
|
||||||
channel_uid, user_uid, message, is_final
|
channel_uid, user_uid, message, is_final
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,7 @@ class UserService(BaseService):
|
|||||||
|
|
||||||
async def search(self, query, **kwargs):
|
async def search(self, query, **kwargs):
|
||||||
query = query.strip().lower()
|
query = query.strip().lower()
|
||||||
|
kwarggs["deleted_at"] = None
|
||||||
if not query:
|
if not query:
|
||||||
return []
|
return []
|
||||||
results = []
|
results = []
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
import { app } from "./app.js";
|
import { app } from "./app.js";
|
||||||
import { NjetComponent } from "./njet.js";
|
import { NjetComponent,eventBus } from "./njet.js";
|
||||||
import { FileUploadGrid } from "./file-upload-grid.js";
|
import { FileUploadGrid } from "./file-upload-grid.js";
|
||||||
|
|
||||||
class ChatInputComponent extends NjetComponent {
|
class ChatInputComponent extends NjetComponent {
|
||||||
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 = []
|
|
||||||
textarea = null
|
users = [];
|
||||||
_value = ""
|
textarea = null;
|
||||||
lastUpdateEvent = null
|
_value = "";
|
||||||
|
lastUpdateEvent = null;
|
||||||
expiryTimer = null;
|
expiryTimer = null;
|
||||||
queuedMessage = null;
|
queuedMessage = null;
|
||||||
lastMessagePromise = null;
|
lastMessagePromise = null;
|
||||||
@ -38,7 +39,7 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get allAutoCompletions() {
|
get allAutoCompletions() {
|
||||||
return Object.assign({}, this.autoCompletions, this.hiddenCompletions)
|
return Object.assign({}, this.autoCompletions, this.hiddenCompletions);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveAutoComplete(input) {
|
resolveAutoComplete(input) {
|
||||||
@ -65,7 +66,7 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAuthors() {
|
getAuthors() {
|
||||||
return this.users.flatMap((user) => [user.username, user.nick])
|
return this.users.flatMap((user) => [user.username, user.nick]);
|
||||||
}
|
}
|
||||||
|
|
||||||
extractMentions(text) {
|
extractMentions(text) {
|
||||||
@ -82,17 +83,14 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
const lowerAuthor = author.toLowerCase();
|
const lowerAuthor = author.toLowerCase();
|
||||||
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
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 };
|
return { mention, closestAuthor, distance: minDistance };
|
||||||
@ -128,7 +126,6 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
return matrix[b.length][a.length];
|
return matrix[b.length][a.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
replaceMentionsWithAuthors(text) {
|
replaceMentionsWithAuthors(text) {
|
||||||
const authors = this.getAuthors();
|
const authors = this.getAuthors();
|
||||||
const mentions = this.extractMentions(text);
|
const mentions = this.extractMentions(text);
|
||||||
@ -143,21 +140,19 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
return updatedText;
|
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;
|
||||||
})
|
});
|
||||||
|
|
||||||
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");
|
||||||
@ -181,24 +176,36 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
this.dispatchEvent(new CustomEvent("uploaded", e));
|
this.dispatchEvent(new CustomEvent("uploaded", e));
|
||||||
});
|
});
|
||||||
this.uploadButton.addEventListener("click", (e) => {
|
this.uploadButton.addEventListener("click", (e) => {
|
||||||
// e.preventDefault();
|
e.preventDefault();
|
||||||
// this.fileUploadGrid.openFileDialog()
|
this.fileUploadGrid.openFileDialog();
|
||||||
|
});
|
||||||
})
|
eventBus.subscribe("file-uploading", (e) => {
|
||||||
this.subscribe("file-uploading", (e) => {
|
|
||||||
|
|
||||||
this.fileUploadGrid.style.display = "block";
|
this.fileUploadGrid.style.display = "block";
|
||||||
|
|
||||||
this.uploadButton.style.display = "none";
|
this.uploadButton.style.display = "none";
|
||||||
this.textarea.style.display = "none";
|
this.textarea.style.display = "none";
|
||||||
})
|
})
|
||||||
|
document.eventBus = eventBus;
|
||||||
this.appendChild(this.uploadButton);
|
this.appendChild(this.uploadButton);
|
||||||
|
|
||||||
this.textarea.addEventListener("blur", () => {
|
this.textarea.addEventListener("blur", () => {
|
||||||
this.updateFromInput("");
|
this.updateFromInput("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventBus.subscribe("file-uploads-done", (data)=>{
|
||||||
|
console.info("JEEJ", data)
|
||||||
|
this.textarea.style.display = "block";
|
||||||
|
this.uploadButton.style.display = "block";
|
||||||
|
this.fileUploadGrid.style.display = "none";
|
||||||
|
let message =data.reduce((file) => {
|
||||||
|
return `${message}[${file.filename}](/channel/attachment/${file.file})`;
|
||||||
|
}, '');
|
||||||
|
app.rpc.sendMessage(this.channelUid, message, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.textarea.addEventListener("keyup", (e) => {
|
this.textarea.addEventListener("keyup", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
|
||||||
const message = this.replaceMentionsWithAuthors(this.value);
|
const message = this.replaceMentionsWithAuthors(this.value);
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
|
|
||||||
@ -210,12 +217,10 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
autoCompletionHandler();
|
autoCompletionHandler();
|
||||||
this.value = "";
|
this.value = "";
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.finalizeMessage(this.messageUid)
|
this.finalizeMessage(this.messageUid);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,9 +259,10 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
}, '');
|
}, '');
|
||||||
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) {
|
||||||
@ -278,33 +284,16 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
flagTyping() {
|
flagTyping() {
|
||||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) {
|
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) {
|
||||||
this.lastUpdateEvent = new Date();
|
this.lastUpdateEvent = new Date();
|
||||||
app.rpc.set_typing(this.channelUid, this.user.color).catch(() => {
|
app.rpc.set_typing(this.channelUid, this.user?.color).catch(() => {});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalizeMessage(messageUid) {
|
async finalizeMessage(messageUid) {
|
||||||
if (!messageUid) {
|
await app.rpc.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(this.value), true);
|
||||||
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.value = "";
|
||||||
this.messageUid = null;
|
this.messageUid = null;
|
||||||
this.queuedMessage = null;
|
this.queuedMessage = null;
|
||||||
this.lastMessagePromise = null
|
this.lastMessagePromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromInput(value) {
|
updateFromInput(value) {
|
||||||
@ -315,39 +304,12 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
|
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
||||||
this.flagTyping()
|
this.flagTyping();
|
||||||
|
|
||||||
if (this.liveType && value[0] !== "/") {
|
if (this.liveType && value[0] !== "/") {
|
||||||
this.expiryTimer = setTimeout(() => {
|
|
||||||
this.finalizeMessage(this.messageUid)
|
|
||||||
}, this.liveTypeInterval * 1000);
|
|
||||||
|
|
||||||
|
|
||||||
const messageText = this.replaceMentionsWithAuthors(value);
|
const messageText = this.replaceMentionsWithAuthors(value);
|
||||||
if (this.messageUid?.startsWith("?")) {
|
this.messageUid = this.sendMessage(this.channelUid, messageText, !this.liveType);
|
||||||
this.queuedMessage = messageText;
|
return this.messageUid;
|
||||||
} 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,3 +322,4 @@ class ChatInputComponent extends NjetComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("chat-input", ChatInputComponent);
|
customElements.define("chat-input", ChatInputComponent);
|
||||||
|
|
||||||
|
226
src/snek/static/editor.js
Normal file
226
src/snek/static/editor.js
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { NjetComponent} from "/njet.js"
|
||||||
|
|
||||||
|
class NjetEditor extends NjetComponent {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#editor {
|
||||||
|
padding: 1rem;
|
||||||
|
outline: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
#command-line {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.2rem 1rem;
|
||||||
|
background: #333;
|
||||||
|
color: #0f0;
|
||||||
|
display: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.editor = document.createElement('div');
|
||||||
|
this.editor.id = 'editor';
|
||||||
|
this.editor.contentEditable = true;
|
||||||
|
this.editor.innerText = `Welcome to VimEditor Component
|
||||||
|
Line 2 here
|
||||||
|
Another line
|
||||||
|
Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
|
||||||
|
|
||||||
|
this.cmdLine = document.createElement('div');
|
||||||
|
this.cmdLine.id = 'command-line';
|
||||||
|
this.shadowRoot.append(style, this.editor, this.cmdLine);
|
||||||
|
|
||||||
|
this.mode = 'normal'; // normal | insert | visual | command
|
||||||
|
this.keyBuffer = '';
|
||||||
|
this.lastDeletedLine = '';
|
||||||
|
this.yankedLine = '';
|
||||||
|
|
||||||
|
this.editor.addEventListener('keydown', this.handleKeydown.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCaretOffset() {
|
||||||
|
let caretOffset = 0;
|
||||||
|
const sel = this.shadowRoot.getSelection();
|
||||||
|
if (!sel || sel.rangeCount === 0) return 0;
|
||||||
|
|
||||||
|
const range = sel.getRangeAt(0);
|
||||||
|
const preCaretRange = range.cloneRange();
|
||||||
|
preCaretRange.selectNodeContents(this.editor);
|
||||||
|
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
caretOffset = preCaretRange.toString().length;
|
||||||
|
return caretOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCaretOffset(offset) {
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = this.shadowRoot.getSelection();
|
||||||
|
const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false);
|
||||||
|
|
||||||
|
let currentOffset = 0;
|
||||||
|
let node;
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
if (currentOffset + node.length >= offset) {
|
||||||
|
range.setStart(node, offset - currentOffset);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentOffset += node.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeydown(e) {
|
||||||
|
const key = e.key;
|
||||||
|
|
||||||
|
if (this.mode === 'insert') {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.editor.blur();
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode === 'command') {
|
||||||
|
if (key === 'Enter' || key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.cmdLine.style.display = 'none';
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mode === 'visual') {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle normal mode
|
||||||
|
this.keyBuffer += key;
|
||||||
|
|
||||||
|
const text = this.editor.innerText;
|
||||||
|
const caretPos = this.getCaretOffset();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let charCount = 0, lineIdx = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (caretPos <= charCount + lines[i].length) {
|
||||||
|
lineIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
charCount += lines[i].length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetToLine = idx =>
|
||||||
|
text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0);
|
||||||
|
|
||||||
|
switch (this.keyBuffer) {
|
||||||
|
case 'i':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'insert';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'v':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'visual';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ':':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'command';
|
||||||
|
this.cmdLine.style.display = 'block';
|
||||||
|
this.cmdLine.textContent = ':';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'yy':
|
||||||
|
e.preventDefault();
|
||||||
|
this.yankedLine = lines[lineIdx];
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dd':
|
||||||
|
e.preventDefault();
|
||||||
|
this.lastDeletedLine = lines[lineIdx];
|
||||||
|
lines.splice(lineIdx, 1);
|
||||||
|
this.editor.innerText = lines.join('\n');
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx));
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'p':
|
||||||
|
e.preventDefault();
|
||||||
|
const lineToPaste = this.yankedLine || this.lastDeletedLine;
|
||||||
|
if (lineToPaste) {
|
||||||
|
lines.splice(lineIdx + 1, 0, lineToPaste);
|
||||||
|
this.editor.innerText = lines.join('\n');
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx + 1));
|
||||||
|
}
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '0':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx));
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gg':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(0);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'G':
|
||||||
|
e.preventDefault();
|
||||||
|
this.setCaretOffset(text.length);
|
||||||
|
this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault();
|
||||||
|
this.mode = 'normal';
|
||||||
|
this.keyBuffer = '';
|
||||||
|
this.cmdLine.style.display = 'none';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// allow up to 2 chars for combos
|
||||||
|
if (this.keyBuffer.length > 2) this.keyBuffer = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('njet-editor', NjetEditor);
|
||||||
|
export {NjetEditor}
|
@ -57,6 +57,7 @@ class FileUploadGrid extends NjetComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset(){
|
reset(){
|
||||||
|
this.uploadResponses = [];
|
||||||
this.uploadsDone = 0;
|
this.uploadsDone = 0;
|
||||||
this.uploadsStarted = 0;
|
this.uploadsStarted = 0;
|
||||||
this._grid.innerHTML = '';
|
this._grid.innerHTML = '';
|
||||||
@ -70,6 +71,7 @@ class FileUploadGrid extends NjetComponent {
|
|||||||
this.reset()
|
this.reset()
|
||||||
this.uploadsDone = 0;
|
this.uploadsDone = 0;
|
||||||
this.uploadsStarted = files.length;
|
this.uploadsStarted = files.length;
|
||||||
|
|
||||||
[...files].forEach(file => this.createTile(file));
|
[...files].forEach(file => this.createTile(file));
|
||||||
}
|
}
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@ -130,6 +132,7 @@ class FileUploadGrid extends NjetComponent {
|
|||||||
startUpload(file, tile, progress) {
|
startUpload(file, tile, progress) {
|
||||||
|
|
||||||
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
||||||
|
console.info("File uploading",file)
|
||||||
|
|
||||||
const protocol = location.protocol === "https:" ? "wss://" : "ws://";
|
const protocol = location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
const ws = new WebSocket(`${protocol}${location.host}/channel/${this.channelUid}/attachment.sock`);
|
const ws = new WebSocket(`${protocol}${location.host}/channel/${this.channelUid}/attachment.sock`);
|
||||||
@ -148,7 +151,7 @@ class FileUploadGrid extends NjetComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
|
console.info(event.data)
|
||||||
|
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
if (data.type === 'progress') {
|
if (data.type === 'progress') {
|
||||||
@ -156,14 +159,20 @@ class FileUploadGrid extends NjetComponent {
|
|||||||
progress.style.width = pct + '%';
|
progress.style.width = pct + '%';
|
||||||
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
this.publish('file-uploading', {file: file, tile: tile, progress: progress});
|
||||||
} else if (data.type === 'done') {
|
} else if (data.type === 'done') {
|
||||||
|
console.info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH")
|
||||||
|
console.info("Done")
|
||||||
|
console.info(this.uploadResponses)
|
||||||
this.uploadsDone += 1;
|
this.uploadsDone += 1;
|
||||||
this.publish('file-uploaded', {file: file, tile: tile, progress: progress});
|
this.publish('file-uploaded', {file: file, tile: tile, progress: progress});
|
||||||
progress.style.width = '100%';
|
progress.style.width = '100%';
|
||||||
tile.classList.add('fug-done');
|
tile.classList.add('fug-done');
|
||||||
|
console.info("Closed")
|
||||||
ws.close();
|
ws.close();
|
||||||
|
this.uploadResponses.push({file:file, remoteFile:data.file})
|
||||||
|
console.info(this.uploadsDone, this.uploadsStarted)
|
||||||
|
if(this.uploadsDone == this.uploadsStarted){
|
||||||
|
this.publish('file-uploads-done', this.uploadResponses);
|
||||||
|
}
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -210,19 +210,18 @@ class Njet extends HTMLElement {
|
|||||||
customElements.define(name, component);
|
customElements.define(name, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(config = {}) {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
if (!Njet._root) {
|
if (!Njet._root) {
|
||||||
Njet._root = this
|
Njet._root = this
|
||||||
Njet._rest = new RestClient({ baseURL: config.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');
|
||||||
this.config = config;
|
|
||||||
this.render.call(this);
|
this.render.call(this);
|
||||||
this.initProps(config);
|
//this.initProps(config);
|
||||||
if (typeof this.construct === 'function')
|
//if (typeof this.config.construct === 'function')
|
||||||
this.construct.call(this)
|
// this.config.construct.call(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
initProps(config) {
|
initProps(config) {
|
||||||
@ -284,9 +283,12 @@ class Njet extends HTMLElement {
|
|||||||
|
|
||||||
render() {}
|
render() {}
|
||||||
}
|
}
|
||||||
|
Njet.registerComponent('njet-root', Njet);
|
||||||
|
|
||||||
class Component extends Njet {}
|
class Component extends Njet {}
|
||||||
|
|
||||||
|
Njet.registerComponent('njet-component', Component);
|
||||||
|
|
||||||
class NjetPanel extends Component {
|
class NjetPanel extends Component {
|
||||||
render() {
|
render() {
|
||||||
this.innerHTML = '';
|
this.innerHTML = '';
|
||||||
@ -491,18 +493,67 @@ 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()
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EventBus extends EventTarget {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.eventMap = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(eventName, callback) {
|
||||||
|
this.addEventListener(eventName, callback);
|
||||||
|
|
||||||
|
if (!this.eventMap.has(eventName)) {
|
||||||
|
this.eventMap.set(eventName, []);
|
||||||
|
}
|
||||||
|
this.eventMap.get(eventName).push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
publish(eventName, detail = {}) {
|
||||||
|
const event = new CustomEvent(eventName, {
|
||||||
|
detail,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(eventName, callback) {
|
||||||
|
this.removeEventListener(eventName, callback);
|
||||||
|
|
||||||
|
const subscribers = this.eventMap.get(eventName);
|
||||||
|
if (subscribers) {
|
||||||
|
const index = subscribers.indexOf(callback);
|
||||||
|
if (index > -1) subscribers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventBus = new EventBus()
|
||||||
|
|
||||||
|
|
||||||
njet.showWindow = function(args) {
|
njet.showWindow = function(args) {
|
||||||
const w = new NjetWindow(args)
|
const w = new NjetWindow(args)
|
||||||
w.show()
|
w.show()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
njet.publish = function(event, data) {
|
||||||
|
if (this.root._subscriptions[event]) {
|
||||||
|
this.root._subscriptions[event].forEach(callback => callback(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
njet.subscribe = function(event, callback) {
|
||||||
|
if (!this.root._subscriptions[event]) {
|
||||||
|
this.root._subscriptions[event] = []
|
||||||
|
}
|
||||||
|
this.root._subscriptions[event].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
window.njet = njet
|
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow,eventBus };
|
||||||
|
|
||||||
export { Njet, NjetButton, NjetPanel, NjetDialog, NjetGrid, NjetComponent, njet, NjetWindow };
|
|
||||||
|
@ -112,9 +112,9 @@ class UploadButtonElement extends HTMLElement {
|
|||||||
this.channelUid = this.getAttribute("channel");
|
this.channelUid = this.getAttribute("channel");
|
||||||
this.uploadButton = this.container.querySelector(".upload-button");
|
this.uploadButton = this.container.querySelector(".upload-button");
|
||||||
this.fileInput = this.container.querySelector(".hidden-input");
|
this.fileInput = this.container.querySelector(".hidden-input");
|
||||||
this.uploadButton.addEventListener("click", () => {
|
/*this.uploadButton.addEventListener("click", () => {
|
||||||
this.fileInput.click();
|
this.fileInput.click();
|
||||||
});
|
});*/
|
||||||
this.fileInput.addEventListener("change", () => {
|
this.fileInput.addEventListener("change", () => {
|
||||||
this.uploadFiles();
|
this.uploadFiles();
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ class Cache:
|
|||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.max_items = max_items
|
self.max_items = max_items
|
||||||
self.stats = {}
|
self.stats = {}
|
||||||
self.enabled = False
|
self.enabled = True
|
||||||
self.lru = []
|
self.lru = []
|
||||||
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
|
self.version = ((42 + 420 + 1984 + 1990 + 10 + 6 + 71 + 3004 + 7245) ^ 1337) + 4
|
||||||
|
|
||||||
|
@ -25,9 +25,20 @@ 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):
|
||||||
|
use_semaphore = kwargs.pop("use_semaphore", False)
|
||||||
|
if use_semaphore:
|
||||||
async with self.semaphore:
|
async with self.semaphore:
|
||||||
return func(*args, **kwargs)
|
database_exception = None
|
||||||
# return await self.loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
for x in range(20):
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
self.db.commit()
|
||||||
|
return result
|
||||||
|
except Exception as ex:
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
database_exception = ex
|
||||||
|
raise database_exception
|
||||||
|
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)
|
||||||
@ -39,7 +50,8 @@ class BaseMapper:
|
|||||||
async def get(self, uid: str = None, **kwargs) -> BaseModel:
|
async def get(self, uid: str = None, **kwargs) -> BaseModel:
|
||||||
if uid:
|
if uid:
|
||||||
kwargs["uid"] = uid
|
kwargs["uid"] = uid
|
||||||
|
if not kwargs.get("deleted_at"):
|
||||||
|
kwargs["deleted_at"] = None
|
||||||
record = await self.run_in_executor(self.table.find_one, **kwargs)
|
record = await self.run_in_executor(self.table.find_one, **kwargs)
|
||||||
if not record:
|
if not record:
|
||||||
return None
|
return None
|
||||||
@ -48,7 +60,6 @@ class BaseMapper:
|
|||||||
for key, value in record.items():
|
for key, value in record.items():
|
||||||
model[key] = value
|
model[key] = value
|
||||||
return model
|
return model
|
||||||
return await self.model_class.from_record(mapper=self, record=record)
|
|
||||||
|
|
||||||
async def exists(self, **kwargs):
|
async def exists(self, **kwargs):
|
||||||
return await self.run_in_executor(self.table.exists, **kwargs)
|
return await self.run_in_executor(self.table.exists, **kwargs)
|
||||||
@ -60,26 +71,39 @@ class BaseMapper:
|
|||||||
if not model.record.get("uid"):
|
if not model.record.get("uid"):
|
||||||
raise Exception(f"Attempt to save without uid: {model.record}.")
|
raise Exception(f"Attempt to save without uid: {model.record}.")
|
||||||
model.updated_at.update()
|
model.updated_at.update()
|
||||||
return await self.run_in_executor(self.table.upsert, model.record, ["uid"])
|
return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True)
|
||||||
|
|
||||||
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
async def find(self, **kwargs) -> typing.AsyncGenerator:
|
||||||
if not kwargs.get("_limit"):
|
if not kwargs.get("_limit"):
|
||||||
kwargs["_limit"] = self.default_limit
|
kwargs["_limit"] = self.default_limit
|
||||||
|
if not kwargs.get("deleted_at"):
|
||||||
|
kwargs["deleted_at"] = None
|
||||||
for record in await self.run_in_executor(self.table.find, **kwargs):
|
for record in await self.run_in_executor(self.table.find, **kwargs):
|
||||||
model = await self.new()
|
model = await self.new()
|
||||||
for key, value in record.items():
|
for key, value in record.items():
|
||||||
model[key] = value
|
model[key] = value
|
||||||
yield model
|
yield model
|
||||||
|
|
||||||
|
async def _use_semaphore(self, sql):
|
||||||
|
sql = sql.lower().strip()
|
||||||
|
return "insert" in sql or "update" in sql or "delete" in sql
|
||||||
|
|
||||||
async def query(self, sql, *args):
|
async def query(self, sql, *args):
|
||||||
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, use_semaphore=await self._use_semaphore(sql)):
|
||||||
yield dict(record)
|
yield dict(record)
|
||||||
|
|
||||||
async def update(self, model):
|
async def update(self, model):
|
||||||
|
if not model["deleted_at"] is None:
|
||||||
|
raise Exception("Can't update deleted record.")
|
||||||
model.updated_at.update()
|
model.updated_at.update()
|
||||||
return await self.run_in_executor(self.table.update, model.record, ["uid"])
|
return await self.run_in_executor(self.table.update, model.record, ["uid"],use_semaphore=True)
|
||||||
|
|
||||||
|
async def upsert(self, model):
|
||||||
|
model.updated_at.update()
|
||||||
|
return await self.run_in_executor(self.table.upsert, model.record, ["uid"],use_semaphore=True)
|
||||||
|
|
||||||
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.")
|
||||||
|
kwargs["use_semaphore"] = True
|
||||||
return await self.run_in_executor(self.table.delete, **kwargs)
|
return await self.run_in_executor(self.table.delete, **kwargs)
|
||||||
|
@ -1,35 +1,33 @@
|
|||||||
# Written by retoor@molodetz.nl
|
# Written by retoor@molodetz.nl
|
||||||
|
|
||||||
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
|
# This code provides middleware functions for an aiohttp server to manage and modify CSP, CORS, and authentication headers.
|
||||||
|
|
||||||
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
|
|
||||||
|
|
||||||
# MIT License: This code is distributed under the MIT License.
|
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
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
|
@web.middleware
|
||||||
async def csp_middleware(request, handler):
|
async def csp_middleware(request, handler):
|
||||||
|
nonce = secrets.token_hex(16)
|
||||||
|
origin = request.headers.get('Origin')
|
||||||
|
csp_policy = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
f"script-src 'self' {origin} 'nonce-{nonce}'; "
|
||||||
|
f"style-src 'self' 'unsafe-inline' {origin} 'nonce-{nonce}'; "
|
||||||
|
"img-src *; "
|
||||||
|
"connect-src 'self' https://umami.molodetz.nl; "
|
||||||
|
"font-src *; "
|
||||||
|
"object-src 'none'; "
|
||||||
|
"base-uri 'self'; "
|
||||||
|
"form-action 'self'; "
|
||||||
|
"frame-src 'self'; "
|
||||||
|
"worker-src *; "
|
||||||
|
"media-src *; "
|
||||||
|
"manifest-src 'self';"
|
||||||
|
)
|
||||||
|
request['csp_nonce'] = nonce
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
return response
|
#response.headers['Content-Security-Policy'] = csp_policy
|
||||||
nonce = generate_nonce()
|
|
||||||
response.headers["Content-Security-Policy"] = csp_policy.format(nonce=nonce)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +37,6 @@ async def no_cors_middleware(request, handler):
|
|||||||
response.headers.pop("Access-Control-Allow-Origin", None)
|
response.headers.pop("Access-Control-Allow-Origin", None)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_allow_middleware(request, handler):
|
async def cors_allow_middleware(request, handler):
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
@ -51,7 +48,6 @@ async def cors_allow_middleware(request, handler):
|
|||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def auth_middleware(request, handler):
|
async def auth_middleware(request, handler):
|
||||||
request["user"] = None
|
request["user"] = None
|
||||||
@ -61,7 +57,6 @@ async def auth_middleware(request, handler):
|
|||||||
)
|
)
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def cors_middleware(request, handler):
|
async def cors_middleware(request, handler):
|
||||||
if request.headers.get("Allow"):
|
if request.headers.get("Allow"):
|
||||||
|
@ -40,10 +40,10 @@ class BaseService:
|
|||||||
yield record
|
yield record
|
||||||
|
|
||||||
async def get(self, uid=None, **kwargs):
|
async def get(self, uid=None, **kwargs):
|
||||||
|
kwargs["deleted_at"] = None
|
||||||
if uid:
|
if uid:
|
||||||
if not kwargs:
|
|
||||||
result = await self.cache.get(uid)
|
result = await self.cache.get(uid)
|
||||||
if False and result and result.__class__ == self.mapper.model_class:
|
if result and result.__class__ == self.mapper.model_class:
|
||||||
return result
|
return result
|
||||||
kwargs["uid"] = uid
|
kwargs["uid"] = uid
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class BaseService:
|
|||||||
await self.cache.set(result["uid"], result)
|
await self.cache.set(result["uid"], result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def save(self, model: UserModel):
|
async def save(self, model):
|
||||||
# if model.is_valid: You Know why not
|
# if model.is_valid: You Know why not
|
||||||
if await self.mapper.save(model):
|
if await self.mapper.save(model):
|
||||||
await self.cache.set(model["uid"], model)
|
await self.cache.set(model["uid"], model)
|
||||||
|
@ -151,6 +151,14 @@ SAFE_ATTRIBUTES = {
|
|||||||
"aria-hidden",
|
"aria-hidden",
|
||||||
"aria-label",
|
"aria-label",
|
||||||
"srcset",
|
"srcset",
|
||||||
|
"target",
|
||||||
|
"rel",
|
||||||
|
"referrerpolicy",
|
||||||
|
"controls",
|
||||||
|
"frameborder",
|
||||||
|
"allow",
|
||||||
|
"allowfullscreen",
|
||||||
|
"referrerpolicy",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -180,6 +188,7 @@ def embed_youtube(text):
|
|||||||
or url.hostname
|
or url.hostname
|
||||||
in [
|
in [
|
||||||
"www.youtube.com",
|
"www.youtube.com",
|
||||||
|
"music.youtube.com",
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"www.youtube-nocookie.com",
|
"www.youtube-nocookie.com",
|
||||||
"youtube-nocookie.com",
|
"youtube-nocookie.com",
|
||||||
@ -330,7 +339,7 @@ def embed_url(text):
|
|||||||
attachments = {}
|
attachments = {}
|
||||||
|
|
||||||
for element in soup.find_all("a"):
|
for element in soup.find_all("a"):
|
||||||
if "href" in element.attrs and element.attrs["href"].startswith("http"):
|
if "href" in element.attrs and element.attrs["href"].startswith("http") and ("data-noembed" not in element.attrs):
|
||||||
page_url = urlparse(element.attrs["href"])
|
page_url = urlparse(element.attrs["href"])
|
||||||
page = get_url_content(element.attrs["href"])
|
page = get_url_content(element.attrs["href"])
|
||||||
if page:
|
if page:
|
||||||
@ -447,7 +456,7 @@ def embed_url(text):
|
|||||||
|
|
||||||
description_element.append(
|
description_element.append(
|
||||||
BeautifulSoup(
|
BeautifulSoup(
|
||||||
f"<p class='page-description'>{page_description or "No description available."}</p>",
|
f"<p class='page-description'>{page_description or 'No description available.'}</p>",
|
||||||
"html.parser",
|
"html.parser",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -556,3 +565,4 @@ class PythonExtension(Extension):
|
|||||||
return "".join(to_write)
|
return "".join(to_write)
|
||||||
|
|
||||||
return str(fn(caller()))
|
return str(fn(caller()))
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<script src="/generic-form.js" type="module"></script>
|
<script src="/generic-form.js" type="module"></script>
|
||||||
<script src="/html-frame.js" type="module"></script>
|
<script src="/html-frame.js" type="module"></script>
|
||||||
<script src="/app.js" type="module"></script>
|
<script src="/app.js" type="module"></script>
|
||||||
|
<script src="/editor.js" type="module"></script>
|
||||||
<script src="/file-manager.js" type="module"></script>
|
<script src="/file-manager.js" type="module"></script>
|
||||||
<script src="/user-list.js"></script>
|
<script src="/user-list.js"></script>
|
||||||
<script src="/message-list.js" type="module"></script>
|
<script src="/message-list.js" type="module"></script>
|
||||||
@ -30,7 +31,7 @@
|
|||||||
<link rel="stylesheet" href="/base.css">
|
<link rel="stylesheet" href="/base.css">
|
||||||
<link rel="icon" type="image/png" href="/image/snek_logo_32x32.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="/image/snek_logo_32x32.png" sizes="32x32">
|
||||||
<link rel="icon" type="image/png" href="/image/snek_logo_64x64.png" sizes="64x64">
|
<link rel="icon" type="image/png" href="/image/snek_logo_64x64.png" sizes="64x64">
|
||||||
<script defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea"></script>
|
<script nonce="{{nonce}}" defer src="https://umami.molodetz.nl/script.js" data-website-id="d127c3e4-dc70-4041-a1c8-bcc32c2492ea"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div id="star-tooltip" class="star-tooltip"></div>
|
<div id="star-tooltip" class="star-tooltip"></div>
|
||||||
<div id="star-popup" class="star-popup"></div>
|
<div id="star-popup" class="star-popup"></div>
|
||||||
<script type="module">
|
<script type="module" nonce="{{nonce}}">
|
||||||
import { app } from "/app.js";
|
import { app } from "/app.js";
|
||||||
import {WebTerminal} from "/dumb-term.js";
|
import {WebTerminal} from "/dumb-term.js";
|
||||||
|
|
||||||
|
@ -192,16 +192,22 @@ class ChannelAttachmentUploadView(BaseView):
|
|||||||
channel_uid=channel_uid, name=filename, user_uid=user_uid
|
channel_uid=channel_uid, name=filename, user_uid=user_uid
|
||||||
)
|
)
|
||||||
pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
|
pathlib.Path(attachment["path"]).parent.mkdir(parents=True, exist_ok=True)
|
||||||
async with aiofiles.open(attachment["path"], "wb") as f:
|
|
||||||
|
async with aiofiles.open(attachment["path"], "wb") as file:
|
||||||
|
print("File openend.", filename)
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
if msg.type == web.WSMsgType.BINARY:
|
if msg.type == web.WSMsgType.BINARY:
|
||||||
|
print("Binary",filename)
|
||||||
if file is not None:
|
if file is not None:
|
||||||
await file.write(msg.data)
|
await file.write(msg.data)
|
||||||
await ws.send_json({"type": "progress", "filename": filename, "bytes": file.tell()})
|
await ws.send_json({"type": "progress", "filename": filename, "bytes": await file.tell()})
|
||||||
elif msg.type == web.WSMsgType.TEXT:
|
elif msg.type == web.WSMsgType.TEXT:
|
||||||
|
print("TExt",filename)
|
||||||
|
print(msg.json())
|
||||||
data = msg.json()
|
data = msg.json()
|
||||||
if data.get('type') == 'end':
|
if data.get('type') == 'end':
|
||||||
await ws.send_json({"type": "done", "filename": filename})
|
relative_url = urllib.parse.quote(attachment_record["relative_url"])
|
||||||
|
await ws.send_json({"type": "done", "file": relative_url, "filename": filename})
|
||||||
elif msg.type == web.WSMsgType.ERROR:
|
elif msg.type == web.WSMsgType.ERROR:
|
||||||
break
|
break
|
||||||
return ws
|
return ws
|
||||||
|
@ -30,6 +30,7 @@ class RPCView(BaseView):
|
|||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.user_session = {}
|
self.user_session = {}
|
||||||
self._scheduled = []
|
self._scheduled = []
|
||||||
|
self._finalize_task = None
|
||||||
|
|
||||||
async def _session_ensure(self):
|
async def _session_ensure(self):
|
||||||
uid = await self.view.session_get("uid")
|
uid = await self.view.session_get("uid")
|
||||||
@ -203,6 +204,13 @@ class RPCView(BaseView):
|
|||||||
)
|
)
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
async def clear_channel(self, channel_uid):
|
||||||
|
self._require_login()
|
||||||
|
user = await self.services.user.get(uid=self.user_uid)
|
||||||
|
if not user["is_admin"]:
|
||||||
|
raise Exception("Not allowed")
|
||||||
|
return await self.services.channel_message.clear(channel_uid)
|
||||||
|
|
||||||
async def write_container(self, channel_uid, content,timeout=3):
|
async def write_container(self, channel_uid, content,timeout=3):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
channel_member = await self.services.channel_member.get(
|
channel_member = await self.services.channel_member.get(
|
||||||
@ -259,6 +267,21 @@ class RPCView(BaseView):
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def send_message(self, channel_uid, message, is_final=True):
|
||||||
|
self._require_login()
|
||||||
|
|
||||||
|
#if not is_final:
|
||||||
|
# check_message = await self.services.channel_message.get(channel_uid=channel_uid, user_uid=self.user_uid,is_final=False)
|
||||||
|
# if check_message:
|
||||||
|
# return await self.update_message_text(check_message["uid"], message)
|
||||||
|
is_final = True
|
||||||
|
message = await self.services.chat.send(
|
||||||
|
self.user_uid, channel_uid, message, is_final
|
||||||
|
)
|
||||||
|
|
||||||
|
return message["uid"]
|
||||||
|
|
||||||
|
|
||||||
async def start_container(self, channel_uid):
|
async def start_container(self, channel_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
channel_member = await self.services.channel_member.get(
|
channel_member = await self.services.channel_member.get(
|
||||||
@ -286,6 +309,8 @@ class RPCView(BaseView):
|
|||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
return await self.services.container.get_status(channel_uid)
|
return await self.services.container.get_status(channel_uid)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def finalize_message(self, message_uid):
|
async def finalize_message(self, message_uid):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
message = await self.services.channel_message.get(message_uid)
|
message = await self.services.channel_message.get(message_uid)
|
||||||
@ -301,13 +326,13 @@ class RPCView(BaseView):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def update_message_text(self, message_uid, text):
|
async def update_message_text(self, message_uid, text):
|
||||||
|
async with self.app.no_save():
|
||||||
self._require_login()
|
self._require_login()
|
||||||
message = await self.services.channel_message.get(message_uid)
|
message = await self.services.channel_message.get(message_uid)
|
||||||
if message["user_uid"] != self.user_uid:
|
if message["user_uid"] != self.user_uid:
|
||||||
raise Exception("Not allowed")
|
raise Exception("Not allowed")
|
||||||
|
|
||||||
if message.get_seconds_since_last_update() > 3:
|
if message.get_seconds_since_last_update() > 5:
|
||||||
await self.finalize_message(message["uid"])
|
|
||||||
return {
|
return {
|
||||||
"error": "Message too old",
|
"error": "Message too old",
|
||||||
"seconds_since_last_update": message.get_seconds_since_last_update(),
|
"seconds_since_last_update": message.get_seconds_since_last_update(),
|
||||||
@ -336,13 +361,21 @@ class RPCView(BaseView):
|
|||||||
|
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
async def send_message(self, channel_uid, message, is_final=True):
|
|
||||||
self._require_login()
|
|
||||||
message = await self.services.chat.send(
|
|
||||||
self.user_uid, channel_uid, message, is_final
|
|
||||||
)
|
|
||||||
|
|
||||||
return message["uid"]
|
|
||||||
|
|
||||||
|
async def clear_channel(self, channel_uid):
|
||||||
|
self._require_login()
|
||||||
|
user = await self.services.user.get(uid=self.user_uid)
|
||||||
|
if not user["is_admin"]:
|
||||||
|
raise Exception("Not allowed")
|
||||||
|
channel = await self.services.channel.get(uid=channel_uid)
|
||||||
|
if not channel:
|
||||||
|
raise Exception("Channel not found")
|
||||||
|
channel['history_start'] = datetime.now()
|
||||||
|
await self.services.channel.save(channel)
|
||||||
|
return await self.services.channel_message.clear(channel_uid)
|
||||||
|
|
||||||
|
|
||||||
async def echo(self, *args):
|
async def echo(self, *args):
|
||||||
self._require_login()
|
self._require_login()
|
||||||
|
@ -71,6 +71,7 @@ class WebView(BaseView):
|
|||||||
await self.app.services.channel_member.save(channel_member)
|
await self.app.services.channel_member.save(channel_member)
|
||||||
|
|
||||||
user = await self.services.user.get(uid=self.session.get("uid"))
|
user = await self.services.user.get(uid=self.session.get("uid"))
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
await self.app.services.channel_message.to_extended_dict(message)
|
await self.app.services.channel_message.to_extended_dict(message)
|
||||||
for message in await self.app.services.channel_message.offset(
|
for message in await self.app.services.channel_message.offset(
|
||||||
@ -81,7 +82,7 @@ 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",
|
||||||
|
Loading…
Reference in New Issue
Block a user