28 Commits

Author SHA1 Message Date
085f62aadf core: Move ssb.appendMessageWithIdentity() from JS => C, and fix a permission test lifetime issue along the way.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-23 17:12:38 -05:00
71d556143b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m24s
2025-12-23 16:12:23 -05:00
081bff9a26 ssb: Load names and profile info for users we see who we're not following.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-23 16:09:47 -05:00
f2f4455c82 ssb: Replication fun. Don't request blobs until messages are up to date. Fix multiple issues with blob wants determinating. Fix issues resulting from message added callbacks being merged.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m54s
2025-12-23 12:54:01 -05:00
2d38b3bd61 update: appimagetool 1.9.1.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m23s
Build Tilde Friends / Build-All (push) Successful in 10m12s
2025-12-21 22:26:16 -05:00
3e73c9e00b update: bundletool 1.18.3. 2025-12-21 22:21:41 -05:00
9aa0e2eda4 ssb: Make opening and closing private chats work a little more consistently with the sidebar.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 10m6s
2025-12-21 19:19:54 -05:00
076cc265f8 edit: Make the navigation bar more legible on mobile.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m46s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-21 16:02:29 -05:00
f0ee9808a9 build: nix flake update.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m50s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-21 15:42:58 -05:00
27be6de208 core: Oh yeah, I just made this unused.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-21 15:28:00 -05:00
304bb82c74 prettier: Yuck.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Successful in 9m52s
2025-12-21 15:14:56 -05:00
ec80f27434 core: Simplify/touch up some of the client-side error handling.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-21 15:08:17 -05:00
12733acd9f core: Slight tweaks to the permissions ui.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-21 14:52:31 -05:00
7ba28a269a core: Oops, was terminating queries too early from my earlier error fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m10s
2025-12-21 14:18:47 -05:00
28e6004c91 edit: Reload static html in its iframe instead of the whole window to make the experience smoother.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-21 14:05:25 -05:00
1a1d84e603 welcome: x86_64 is more relevant than 64-bit.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-21 13:42:16 -05:00
da1116220c core: Shutdown paranoia / trying to fix an intermittent test failure.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 11m19s
2025-12-21 10:34:14 -05:00
57c945e2cf core: Fix some longstanding exception plumbing issues.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Failing after 10m48s
2025-12-21 10:08:34 -05:00
bef66329b2 apps: Use the indexes more consistently. 2025-12-21 09:01:03 -05:00
4f726ce502 bookclub: Use the index. 2025-12-21 08:46:55 -05:00
cc0266b5e8 intro: Make the buttons more consistent/less obnoxious.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m28s
Build Tilde Friends / Build-All (push) Successful in 10m16s
2025-12-20 17:40:24 -05:00
dad14d1754 bookclub: Load and display reviews.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 9m57s
2025-12-20 15:58:24 -05:00
c867204233 core: allPermissionsGranted() JS => C. 2025-12-20 14:04:25 -05:00
68eb53b1ff core: Reestablish the websocket connection when disconnected as the mobile app is brought to the front.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m31s
2025-12-20 10:34:47 -05:00
403c5fcfe6 ssb: Fixed some private message types not rendering anything at all.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m37s
2025-12-18 22:29:13 -05:00
c0ed9fda01 test: Post and view a private message in the test.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 11m20s
2025-12-18 12:39:48 -05:00
95d263e139 core: Merge App into the process object.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 20:44:27 -05:00
782013f3a3 docs: Prepare some release notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 3m4s
Build Tilde Friends / Build-All (push) Successful in 9m58s
2025-12-17 20:16:57 -05:00
38 changed files with 1142 additions and 701 deletions

View File

@@ -23,9 +23,9 @@ VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.5
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3510100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.18.2/bundletool-all-1.18.2.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar
APPIMAGETOOL_URL := https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := 43264887ffe43cdc02171b3463912168 out/appimagetool
PROJECT = tildefriends
BUILD_DIR ?= out

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📚",
"previous": "&yLHlvKirJEqrekP5lf5BydvzIo/vN+z7K2ACQacxJXE=.sha256"
"previous": "&EO5ifwzemEeSJsN6SJ2VTyE+sqnwU2gikIngQimwnDo=.sha256"
}

View File

@@ -1,5 +1,3 @@
import * as commonmark from './commonmark.min.js';
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function (row) {
@@ -8,174 +6,66 @@ async function query(sql, args) {
return result;
}
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" title="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" title="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
function markdown(md) {
let reader = new commonmark.Parser();
let writer = new commonmark.HtmlRenderer({safe: true});
//writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}
async function main() {
let data = await query(`
let whoami = await ssb.getActiveIdentity();
let following = Object.keys(await ssb.following([whoami], 2));
let data = await query(
`
SELECT
messages.id,
content ->> 'title' AS title,
content ->> '$.image.link' AS image,
content ->> 'description' AS description
FROM messages
content ->> 'description' AS description,
content ->> 'authors' AS authors
FROM messages, json_each(?) AS following
ON messages.author = following.value
WHERE
content ->> 'type' = 'bookclub' AND
title IS NOT NULL AND
image IS NOT NULL AND
description IS NOT NULL
`);
if (!data?.length) {
await app.setDocument(`
<!DOCTYPE html>
<html>
<body style="background-color: #fff">
<p>No bookclub messages found.</p>
</body>
</html>
`);
return;
`,
[JSON.stringify(following)]
);
let books = Object.fromEntries(data.map((x) => [x.id, x]));
for (let book of Object.values(books)) {
try {
book.authors = JSON.parse(book.authors);
} catch {
book.authors = [book.authors];
}
book.reviews = [];
}
await app.setDocument(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="w3.css">
</head>
<body class="w3-grid" style="background-color: #fff; gap:8px;grid-template-columns:repeat(auto-fit, minmax(4in,1fr))">
${data
.map(
(x) => `
<div class="w3-card-4">
<header class="w3-container w3-center">
<h1>${markdown(x.title)}</h1>
</header>
<div class="w3-container w3-center">
<img src="/${x.image}/view" style="max-height: 2in; max-width: 2in">
</div>
<div class="w3-container">
<p>${markdown(x.description)}</p>
</div>
</div>
`
)
.join('\n')}
</body>
</html>
`);
let reviews = await query(
`
SELECT author, about, json(json_group_object(key, value)) AS content
FROM (
SELECT
messages.author,
messages.content ->> '$.about' AS about,
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.about', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?) AS book, json_each(?) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'about'
AND messages.content ->> '$.about' = book.value
AND NOT fields.key IN ('about', 'type')
) WHERE rank = 1
GROUP BY author, about
`,
[JSON.stringify(Object.keys(books)), JSON.stringify(following)]
);
for (let review of reviews) {
review.content = JSON.parse(review.content);
books[review.about].reviews.push(review);
}
await app.setDocument(
utf8Decode(getFile('index.html')).replace('G_DATA', JSON.stringify(data))
);
}
main();
main().catch(function (e) {
throw new Error(e.message);
});

51
apps/bookclub/bc-app.js Normal file
View File

@@ -0,0 +1,51 @@
import {LitElement, html, map, unsafeHTML} from './lit-all.min.js';
import {markdown} from './markdown.js';
class BookClubElement extends LitElement {
render() {
if (!g_data) {
return html`<h1>No bookclub messages found.</h1>`;
}
return html`
<link rel="stylesheet" href="w3.css"></link>
<div class="w3-grid" style="background-color: #fff; gap:8px; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr))">
${map(
g_data,
(x) => html`
<div class="w3-card-4">
<header class="w3-container w3-center">
<h1>${markdown(x.title)}</h1>
<p>${map(x.authors, (author) => markdown(author))}</p>
</header>
<div class="w3-container w3-center">
<img
src="/${x.image}/view"
style="max-height: 2in; max-width: 2in"
/>
</div>
<div class="w3-container">
<p>${markdown(x.description)}</p>
</div>
<ul class="w3-container w3-list">
${map(
x.reviews.filter(
(x) => x.content?.rating || x.content?.review
),
(review) => html`
<li>
${review.content.rating} /
${review.content.ratingMax ?? 5}
${review.content.ratingType ?? 'stars'}
<div>${review.content.review}</div>
</li>
`
)}
</ul>
</div>
`
)}
</div>`;
}
}
customElements.define('bc-app', BookClubElement);

View File

@@ -1,6 +1,13 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="w3.css" />
<script>
const g_data = G_DATA;
</script>
</head>
<body>
${BODY}
<bc-app />
</body>
<script type="module" src="bc-app.js"></script>
</html>

120
apps/bookclub/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

119
apps/bookclub/markdown.js Normal file
View File

@@ -0,0 +1,119 @@
import * as commonmark from './commonmark.min.js';
import {unsafeHTML} from './lit-all.min.js';
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" title="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" title="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
export function markdown(md) {
let reader = new commonmark.Parser();
let writer = new commonmark.HtmlRenderer({safe: true});
writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return unsafeHTML(writer.render(parsed));
}

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "➡️",
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
"previous": "&WiC0IosFJw/FNuypqKc4LFZUZjCFlmbY7KEAabrXU9o=.sha256"
}

View File

@@ -18,7 +18,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, '$.type') = 'contact'
content ->> 'type' = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]
@@ -149,7 +149,7 @@ async function fetch_about(db, ids, users) {
messages.author = following.value AND
messages.rowid > ?3 AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
messages.content ->> 'type' = 'about'
UNION
SELECT
messages.*
@@ -159,7 +159,7 @@ async function fetch_about(db, ids, users) {
WHERE
messages.author = following.value AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
messages.content ->> 'type' = 'about'
ORDER BY messages.author, messages.sequence
`,
[

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💡",
"previous": "&FGkkfFLaEID3V4lUjPbgCOwgEvNXkcVkzs0zzwD/gQ8=.sha256"
"previous": "&dez1mAjzd4X9Z6ss0cBJO8EJDP+g3GtyYDNiasrw2pM=.sha256"
}

View File

@@ -53,7 +53,7 @@
<div>~😎 Tilde Friends.</div>
</div>
<footer>
<button class="w3-button w3-yellow proceed">Next</button>
<button class="w3-button w3-yellow proceed" id="next0">Next</button>
</footer>
</div>
</div>
@@ -72,7 +72,7 @@
</li>
</ul>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow proceed">Onward</button>
<button class="w3-button w3-yellow proceed" id="next1">Next</button>
</footer>
</div>
<div class="slide w3-gray" style="width: 90%">
@@ -120,7 +120,7 @@
target="_blank"
>See scuttlebutt.nz</a
>
<button class="w3-button w3-yellow proceed">Got It</button>
<button class="w3-button w3-yellow proceed" id="next2">Next</button>
</footer>
</div>
</div>
@@ -159,7 +159,7 @@
</ul>
</div>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow proceed">Okay</button>
<button class="w3-button w3-yellow proceed" id="next3">Next</button>
</footer>
</div>
</div>
@@ -194,12 +194,14 @@
</li>
<li>
To see this tutorial again later, select <b>apps</b> -&gt;
<b>Core Apps</b> -&gt; <b>intro</b>.
<b>Core Apps</b> -&gt; <b>intro</b>. When you continue, you will
be prompted to save a setting so that you don't see this every
time.
</li>
</ul>
</div>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow" id="complete">Let's Go!</button>
<button class="w3-button w3-yellow" id="complete">Continue</button>
</footer>
</div>
</div>

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&eqeAxU0q6n0RZDSd68j44hQ4UtssESqgohsCXN/otwY=.sha256"
"previous": "&S0BuSm19vBzA6Gf97/rYcwndKyb55MxoITnz7xRjlzg=.sha256"
}

View File

@@ -105,10 +105,24 @@ class TfElement extends LitElement {
await this.load_channels();
}
async open_private_chat(event) {
let update = {};
update[event.detail.key] = false;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
);
}
async close_private_chat(event) {
let update = {};
update[event.detail.key] = true;
this.private_closed = Object.assign(update, this.private_closed);
update[
event.detail.key == '[]'
? JSON.stringify([this.whoami])
: event.detail.key
] = true;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
@@ -167,16 +181,22 @@ class TfElement extends LitElement {
return [];
}
let self = this;
let self_key = JSON.stringify([this.whoami]);
let opened = Object.entries(this.private_closed)
.filter(([key, value]) => !value)
.map(([key, value]) => [key, []]);
return Object.fromEntries(
Object.entries(this.grouped_private_messages).filter(([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
})
[...Object.entries(this.grouped_private_messages), ...opened].filter(
([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
}
)
);
}
@@ -191,14 +211,18 @@ class TfElement extends LitElement {
.map((x) => '🔐' + JSON.parse(x).join(',')),
...this.channels.map((x) => '#' + x),
];
let index = channel_names.indexOf(this.hash.substring(1));
let lookup = this.hash.substring(1);
if (lookup == '🔐') {
lookup = '🔐' + this.whoami;
}
let index = channel_names.indexOf(lookup);
index = index != -1 ? index + delta : 0;
tfrpc.rpc.setHash(
'#' +
encodeURIComponent(
channel_names[(index + channel_names.length) % channel_names.length]
)
);
let name =
channel_names[(index + channel_names.length) % channel_names.length];
if (name == '🔐' + this.whoami) {
name = '🔐';
}
tfrpc.rpc.setHash('#' + encodeURIComponent(name));
}
set_hash(hash) {
@@ -212,7 +236,7 @@ class TfElement extends LitElement {
}
}
async fetch_about(following, users) {
async fetch_about(following, users, transient) {
this.loading_about++;
let ids = Object.keys(following).sort();
const k_cache_version = 3;
@@ -260,7 +284,7 @@ class TfElement extends LitElement {
fields.value
FROM messages JOIN json_each(messages.content) AS fields
WHERE
messages.content ->> '$.type' = 'about' AND
messages.content ->> 'type' = 'about' AND
messages.content ->> '$.about' = messages.author AND
NOT fields.key IN ('about', 'type')) all_abouts
JOIN json_each(?) AS following ON all_abouts.author = following.value
@@ -296,7 +320,7 @@ class TfElement extends LitElement {
this.loading_about--;
let new_cache = JSON.stringify(cache);
if (new_cache != original_cache) {
if (!transient && new_cache != original_cache) {
let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000);
@@ -581,7 +605,7 @@ class TfElement extends LitElement {
SELECT DISTINCT content ->> '$.vote.expression' AS value
FROM messages
WHERE author = ? AND
content ->> '$.type' = 'vote'
content ->> 'type' = 'vote'
ORDER BY timestamp DESC LIMIT 10
`,
[this.whoami]
@@ -684,6 +708,7 @@ class TfElement extends LitElement {
@refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
@openprivatechat=${this.open_private_chat}
@closeprivatechat=${this.close_private_chat}
.connections=${this.connections}
.private_messages=${this.private_messages}
@@ -777,6 +802,18 @@ class TfElement extends LitElement {
}
}
async request_user(event) {
let users = {};
users[event.detail.id] = {};
users = await this.fetch_user_info(users);
if (this.users[event.detail.id]?.seq !== users[event.detail.id]?.seq) {
let self = this;
this.fetch_about(users, users, true).then(function (result) {
self.users = Object.assign({}, self.users, users);
});
}
}
render() {
let self = this;
@@ -889,6 +926,7 @@ class TfElement extends LitElement {
<div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"
@tf-request-user=${this.request_user}
>
${progress}
<div style="flex: 0 0">${tabs}</div>

View File

@@ -1166,7 +1166,17 @@ class TfMessageElement extends LitElement {
);
}
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
if (this.message?.decrypted) {
if (this.format == 'decrypted') {
return this.render_small_frame(
this.render_json(this.message.decrypted)
);
} else {
return this.render_small_frame(this.message.decrypted.type);
}
} else {
return this.render_small_frame();
}
} else {
return this.render_small_frame(this.render_raw());
}

View File

@@ -241,6 +241,20 @@ class TfProfileElement extends LitElement {
`;
}
open_private_chat() {
let hash = '#🔐' + (this.id != this.whoami ? this.id : '');
this.dispatchEvent(
new CustomEvent('openprivatechat', {
bubbles: true,
composed: true,
detail: {
key: JSON.stringify([this.id]),
},
})
);
tfrpc.rpc.setHash(hash);
}
render() {
this.load();
let self = this;
@@ -360,9 +374,9 @@ class TfProfileElement extends LitElement {
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
<footer class="w3-container">
<p>
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
<button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat">
Open Private Chat
</a>
</button>
${edit}
${follow}
${block}

View File

@@ -200,6 +200,7 @@ class TfTabNewsElement extends LitElement {
}
render_sidebar() {
let self_key = JSON.stringify([this.whoami]);
return html`
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
@@ -254,12 +255,16 @@ class TfTabNewsElement extends LitElement {
?.map(
(key) => html`
<a
href=${'#🔐' + JSON.parse(key).join(',')}
href=${'#🔐' +
(key == self_key ? '' : JSON.parse(key).join(','))}
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' + JSON.parse(key).join(',')
style=${this.hash ==
'#🔐' + (key == self_key ? '' : JSON.parse(key).join(','))
? 'font-weight: bold'
: undefined}
>${this.unread_status('🔐' + JSON.parse(key).join(','))}
>${this.unread_status(
'🔐' + (key == self_key ? '' : JSON.parse(key).join(','))
)}
${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
(id) => html`
<tf-user

View File

@@ -25,6 +25,15 @@ class TfUserElement extends LitElement {
render() {
let user = this.users[this.id];
if (!this.users[this.id]) {
this.dispatchEvent(
new CustomEvent('tf-request-user', {
bubbles: true,
composed: true,
detail: {id: this.id},
})
);
}
let shape =
user?.follow_depth === undefined || user.follow_depth >= 2
? 'w3-circle'

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&m7sF3iaAEqf6PtaTBN+upiHNNBPjrq15kZUuxRWiAag=.sha256"
"previous": "&IwbeqN5jcUWa8wbnWxduiwel9VldRO/dARDjljX33PM=.sha256"
}

View File

@@ -152,7 +152,7 @@
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
Get Linux x86_64 AppImage
</a>
</p>
<p>

View File

@@ -272,12 +272,20 @@ class TfNavigationElement extends LitElement {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div
style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%"
style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4)"
@click=${() => (this.show_permissions = false)}
>
<div
style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff"
style="position: absolute; background-color: #444; padding: 1em; margin: auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff"
@click=${(event) => event.stopPropagation()}
>
<div>This app has the following permissions:</div>
<div>
This app at <code>${window.location.pathname}</code> has the
following permissions:
</div>
${Object.keys(this.permissions).length == 0
? html`<p class="w3-container">(no permissions)</p>`
: undefined}
${Object.keys(this.permissions).map(
(key) => html`
<div>
@@ -426,13 +434,35 @@ class TfNavigationElement extends LitElement {
</div>
${this.status?.is_error
? html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">&times;</span>
<div style="color: ${this.status.color ?? k_color_error}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
<style>
#error {
white-space: pre;
}
</style>
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div
class="w3-modal"
style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1; display: flex; justify-content: center"
>
<div
class="w3-modal-content w3-card-4 w3-animate-top"
style="display: block; position: absolute; padding: 1em; top: 0"
>
<span
id="close_error"
@click=${self.clear_error}
class="w3-button w3-display-topright w3-red"
>&times;</span
>
<div
style="color: ${this.status.color ??
k_color_error}; display: block"
>
<b>ERROR:</b>
<p id="error">${this.status.message}</p>
</div>
</div>
</div>
</div>
`
: undefined}
`;
@@ -995,6 +1025,32 @@ function closeEditor() {
document.getElementById('viewPane').style.display = 'flex';
}
/**
* Reload any static HTML content in the iframe.
*/
async function update_html() {
try {
let response = await fetch(window.location.href);
let text = await response.text();
let parser = new DOMParser();
let new_doc = parser.parseFromString(text, 'text/html');
let new_html = new_doc.getElementById('document').attributes.srcdoc.value;
let iframe = document.getElementById('document');
let sandbox = iframe.sandbox;
let iframe_parent = iframe.parentNode;
iframe_parent.removeChild(iframe);
let new_iframe = document.createElement('iframe');
new_iframe.sandbox = sandbox;
new_iframe.id = 'document';
new_iframe.srcdoc = new_html;
iframe_parent.appendChild(new_iframe);
} catch (e) {
alert(error);
}
}
/**
* Save the app.
* @param save_to An optional path to which to save the app.
@@ -1090,7 +1146,7 @@ function save(save_to) {
if (save_path != window.location.pathname) {
alert('Saved to ' + save_path + '.');
} else if (!g_files['app.js']) {
window.location.reload();
update_html();
} else {
reconnect(save_path);
}
@@ -1469,6 +1525,18 @@ function blur() {
send({event: 'blur'});
}
/**
* Notify the app of visibility change. Seems to work when changing apps/tabs
* where focus/blur doesn't on mobile.
*/
function visibilitychange() {
if (!document.hidden) {
if (g_socket && g_socket.readyState == g_socket.CLOSED) {
connectSocket();
}
}
}
/**
* Handle a message.
* @param event The message.
@@ -1857,6 +1925,7 @@ window.addEventListener('load', function () {
window.addEventListener('beforeunload', function () {
g_unloading = true;
});
document.addEventListener('visibilitychange', visibilitychange);
document.getElementById('name').value = window.location.pathname;
document
.getElementById('closeEditor')

View File

@@ -14,80 +14,6 @@ let g_handler_index = 0;
/** Whether updating accounts information is currently scheduled. */
let g_update_accounts_scheduled;
/**
** App constructor.
** @return An app instance.
*/
function App() {
this._send_queue = [];
this.calls = {};
this._next_call_id = 1;
return this;
}
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
App.prototype.makeFunction = function (api) {
let self = this;
let result = function () {
let id = self._next_call_id++;
while (!id || self.calls[id]) {
id = self._next_call_id++;
}
let promise = new Promise(function (resolve, reject) {
self.calls[id] = {resolve: resolve, reject: reject};
});
let message = {
action: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
self.send(message);
return promise;
};
Object.defineProperty(result, 'name', {value: api[0], writable: false});
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
App.prototype.send = function (message) {
if (this._send_queue) {
if (this._on_output) {
this._send_queue.forEach((x) => this._on_output(x));
this._send_queue = null;
} else if (message) {
this._send_queue.push(message);
}
}
if (message && this._on_output) {
this._on_output(message);
}
};
/**
* Print an error.
* @param error The error.
*/
function printError(error) {
if (error.stackTrace) {
print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
print(error.stackTrace);
} else {
for (let [k, v] of Object.entries(error)) {
print(k, v);
}
print(error.toString());
}
}
/**
* Invoke a handler.
* @param handlers The handlers on which to invoke the callback.
@@ -233,7 +159,62 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
process.url = options?.url;
process.eventHandlers = {};
if (!options?.script || options?.script === 'app.js') {
process.app = new App();
process._send_queue = [];
process._calls = {};
process._next_call_id = 1;
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
process.makeFunction = function (api) {
let result = function () {
let id = process._next_call_id++;
while (!id || process._calls[id]) {
id = process._next_call_id++;
}
let promise = new Promise(function (resolve, reject) {
process._calls[id] = {resolve: resolve, reject: reject};
});
let message = {
action: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
process.send(message);
return promise;
};
Object.defineProperty(result, 'name', {
value: api[0],
writable: false,
});
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
process.send = function (message) {
if (process._send_queue) {
if (process._on_output) {
process._send_queue.forEach((x) => process._on_output(x));
process._send_queue = null;
} else if (message) {
process._send_queue.push(message);
}
}
if (message && process._on_output) {
process._on_output(message);
}
};
} else {
process.makeFunction = function (api) {
return function () {};
};
}
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
@@ -248,19 +229,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
core: {
broadcast: broadcast.bind(process),
user: getUser(process, process),
allPermissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
settings.userPermissions &&
settings.userPermissions[user]
) {
return settings.userPermissions[user];
}
},
permissionTest: async function (permission, description) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
@@ -286,8 +254,8 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
} else {
throw Error(`Permission denied: ${permission}.`);
}
} else if (process.app) {
return process.app
} else {
return process
.makeFunction(['requestPermission'])(permission, description)
.then(async function (value) {
if (value == 'allow') {
@@ -317,8 +285,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
}
throw Error(`Permission denied: ${permission}.`);
});
} else {
throw Error(`Permission denied: ${permission}.`);
}
},
},
@@ -331,7 +297,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
);
let json = JSON.stringify(identities);
if (process._last_sent_identities !== json) {
process.app.send(
process.send(
Object.assign(
{
action: 'identities',
@@ -393,7 +359,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
imports.app = {};
for (let i in options.api) {
let api = options.api[i];
imports.app[api[0]] = process.app.makeFunction(api);
imports.app[api[0]] = process.makeFunction(api);
}
}
for (let [name, f] of Object.entries(options?.imports || {})) {
@@ -404,17 +370,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
imports.app.print(...args);
}
};
process.task.onError = function (error) {
try {
if (process.app) {
process.app.makeFunction(['error'])(error);
} else {
printError(error);
}
} catch (e) {
printError(error);
}
};
process.task.onError = process.makeFunction(['error']);
imports.ssb = Object.fromEntries(
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
);
@@ -459,33 +415,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
});
}
};
imports.ssb.appendMessageWithIdentity = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
let action;
try {
if (message?.type === 'vote' && message?.vote?.expression) {
action = `React with ${message?.vote?.expression}.`;
} else if (typeof message === 'string') {
action = `Post a private message.`;
} else {
action = `Publish ${'aeiou'.indexOf(message?.type?.toLowerCase()?.substring(0, 1)) != -1 ? 'an' : 'a'} "${message?.type}" message.`;
}
} catch {}
return Promise.resolve(
imports.core.permissionTest('ssb_append', action)
).then(function () {
return ssb.appendMessageWithIdentity(
process.credentials.session.name,
id,
message
);
});
}
};
if (
process.credentials &&
process.credentials.session &&
@@ -535,7 +464,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
};
}
process.sendPermissions = async function sendPermissions() {
process.app.send({
process.send({
action: 'permissions',
permissions: await imports.core.permissionsGranted(),
});
@@ -566,31 +495,27 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
let source = await ssb.blobGet(blobId);
let appSourceName = blobId;
let appSource = utf8Decode(source);
try {
let appObject = JSON.parse(appSource);
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
await File.readFile('core/tfrpc.js'),
]);
await Promise.all(
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
f,
await ssb.blobGet(appObject.files[f]),
]);
})
);
}
} catch (e) {
printError(e);
let appObject = JSON.parse(appSource);
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
await File.readFile('core/tfrpc.js'),
]);
await Promise.all(
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
f,
await ssb.blobGet(appObject.files[f]),
]);
})
);
}
if (process.app) {
process.app.send({action: 'ready', version: version()});
if (process.send) {
process.send({action: 'ready', version: version()});
await process.sendPermissions();
}
await process.task.execute({name: appSourceName, source: appSource});
@@ -600,12 +525,12 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
sendStats();
}
} catch (error) {
if (process?.app && process?.task?.onError) {
if (process?.task?.onError) {
process.task.onError(error);
} else {
printError(error);
}
rejectReady(error);
if (rejectReady) {
rejectReady(error);
}
}
}
return process;
@@ -673,13 +598,11 @@ async function loadSettings() {
* Send periodic stats to all clients.
*/
function sendStats() {
let apps = Object.values(gProcesses)
.filter((process) => process.app)
.map((process) => process.app);
let apps = Object.values(gProcesses).filter((process) => process.send);
if (apps.length) {
let stats = getStats();
for (let app of apps) {
app.send({action: 'stats', stats: stats});
for (let process of apps) {
process.send({action: 'stats', stats: stats});
}
setTimeout(sendStats, 1000);
} else {

View File

@@ -66,7 +66,7 @@
class="vbox"
style="flex: 0 1 100%; display: none; overflow: auto"
>
<div class="navigation w3-bar" style="display: flex">
<div class="w3-bar w3-blue">
<button
class="w3-bar-item w3-button w3-blue"
id="closeEditor"
@@ -77,16 +77,6 @@
>
Close
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="save"
name="save"
accesskey="s"
onmouseover="set_access_key_title(event)"
data-tip="Save the app under the given path"
>
Save
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="icon"
@@ -137,13 +127,6 @@
>
</button>
<input
class="w3-bar-item w3-input w3-border w3-blue"
type="text"
id="name"
name="name"
style="flex: 1 1; min-width: 1em"
/>
<button
class="w3-bar-item w3-button w3-blue"
id="delete"
@@ -163,6 +146,23 @@
>
Trace
</button>
<button
class="w3-bar-item w3-button w3-blue w3-right"
id="save"
name="save"
accesskey="s"
onmouseover="set_access_key_title(event)"
data-tip="Save the app under the given path"
>
Save
</button>
<input
class="w3-bar-item w3-input w3-cobalt w3-right"
type="text"
id="name"
name="name"
style="min-width: 1em"
/>
</div>
<div class="hbox" style="flex: 1 1; overflow: auto">
<div style="overflow: auto">
@@ -178,7 +178,6 @@
<iframe
id="document"
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
style="width: 100%; height: 100%; border: 0"
></iframe>
</div>
</div>

View File

@@ -154,3 +154,9 @@ body {
margin: 0 auto;
max-width: 80%;
}
#document {
width: 100%;
height: 100%;
border: 0;
}

File diff suppressed because one or more lines are too long

222
deps/codemirror_src/package-lock.json generated vendored
View File

@@ -129,14 +129,14 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
@@ -165,9 +165,9 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -186,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.39.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
"version": "6.39.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.6.tgz",
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -248,9 +248,9 @@
}
},
"node_modules/@lezer/common": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz",
"integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
@@ -274,9 +274,9 @@
}
},
"node_modules/@lezer/html": {
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
"integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -316,12 +316,12 @@
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.1.tgz",
"integrity": "sha512-72ah+Sml7lD8Wn7lnz9vwYmZBo9aQT+I2gjK/0epI+gjdwUbWw3MJ/ZBGEqG1UfrIauRqH37/c5mVHXeCTGXtA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.2.tgz",
"integrity": "sha512-iNSdKrIK0FfOjVPVpV0fu7OykdncYpEzf4vkG9szFf60ql/ObZShoVbM9u1tgkogDOmubms1CyoNS2/unOXWNw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0"
}
},
@@ -412,9 +412,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"cpu": [
"arm"
],
@@ -425,9 +425,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"cpu": [
"arm64"
],
@@ -438,9 +438,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"cpu": [
"arm64"
],
@@ -451,9 +451,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"cpu": [
"x64"
],
@@ -464,9 +464,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"cpu": [
"arm64"
],
@@ -477,9 +477,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"cpu": [
"x64"
],
@@ -490,9 +490,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"cpu": [
"arm"
],
@@ -503,9 +503,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"cpu": [
"arm"
],
@@ -516,9 +516,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"cpu": [
"arm64"
],
@@ -529,9 +529,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"cpu": [
"arm64"
],
@@ -542,9 +542,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"cpu": [
"loong64"
],
@@ -555,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"cpu": [
"ppc64"
],
@@ -568,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"cpu": [
"riscv64"
],
@@ -581,9 +581,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"cpu": [
"riscv64"
],
@@ -594,9 +594,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"cpu": [
"s390x"
],
@@ -607,9 +607,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
@@ -620,9 +620,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
@@ -633,9 +633,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"cpu": [
"arm64"
],
@@ -646,9 +646,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"cpu": [
"arm64"
],
@@ -659,9 +659,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"cpu": [
"ia32"
],
@@ -672,9 +672,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"cpu": [
"x64"
],
@@ -685,9 +685,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"cpu": [
"x64"
],
@@ -877,9 +877,9 @@
}
},
"node_modules/rollup": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -892,28 +892,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.5",
"@rollup/rollup-android-arm64": "4.53.5",
"@rollup/rollup-darwin-arm64": "4.53.5",
"@rollup/rollup-darwin-x64": "4.53.5",
"@rollup/rollup-freebsd-arm64": "4.53.5",
"@rollup/rollup-freebsd-x64": "4.53.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
"@rollup/rollup-linux-arm64-musl": "4.53.5",
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
"@rollup/rollup-linux-x64-gnu": "4.53.5",
"@rollup/rollup-linux-x64-musl": "4.53.5",
"@rollup/rollup-openharmony-arm64": "4.53.5",
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
"@rollup/rollup-win32-x64-gnu": "4.53.5",
"@rollup/rollup-win32-x64-msvc": "4.53.5",
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"fsevents": "~2.3.2"
}
},

View File

@@ -1,6 +1,6 @@
# Release Checklist
- make sure ci is passing
- make sure CI is passing
- run the tests
- format + prettier
- update metadata/en-US/changelogs
@@ -8,12 +8,13 @@
- git tag -f latest_release
- push
- make a release on gitea
- update ios screenshots if UI has substantially changed
- upload the artifacts
- upload the AppImage and zsyncmake
- upload to Google
- upload to Apple with dist-ios on macos
- upload to Apple with dist-iOS on macOS
- nix
- june and december: update release version
- June and December: update release version
- run `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
- comment out the hash in default.nix
- update the version

8
flake.lock generated
View File

@@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1763948260,
"narHash": "sha256-dY9qLD0H0zOUgU3vWacPY6Qc421BeQAfm8kBuBtPVE0=",
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c8ba8d3f7634acac4a2094eef7c32ad9106532c",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -2,7 +2,7 @@
description = "Tilde Friends is a platform for making, running, and sharing web applications.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
};

View File

@@ -0,0 +1,6 @@
* Crash fixes.
* Fix channels with hyphens and various other characters not working correctly.
* Navigation bar and search UI improvements.
* Faster loads, though the first launch may be particularly slow as indexes are rebuilt.
* Fixed various broken links.
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1.

View File

@@ -24,6 +24,8 @@
#include <alloca.h>
#endif
static JSClassID s_permission_test_class_id;
typedef struct _app_path_pair_t
{
const char* app;
@@ -417,6 +419,52 @@ static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst
return result;
}
static void _tf_api_core_all_permissions_granted_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
permissions_granted_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_UNDEFINED;
if (work->settings)
{
JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
if (JS_IsObject(json) && work->user)
{
JSValue user_permissions = JS_GetPropertyStr(context, json, "userPermissions");
if (JS_IsObject(user_permissions))
{
result = JS_GetPropertyStr(context, user_permissions, work->user);
}
JS_FreeValue(context, user_permissions);
}
JS_FreeValue(context, json);
tf_free((void*)work->settings);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
tf_free(work);
}
static JSValue _tf_api_core_allPermissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSValue process = data[0];
permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
*work = (permissions_granted_t) {
.context = context,
.user = _tf_ssb_get_process_credentials_session_name(context, process),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_core_all_permissions_granted_after_work, work);
return result;
}
typedef struct _active_identity_work_t
{
JSContext* context;
@@ -838,38 +886,59 @@ typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSVal
typedef struct _permission_test_t
{
JSContext* context;
bool completed;
permission_test_callback_t* callback;
void* user_data;
} permission_test_t;
static JSValue _tf_ssb_permission_test_resolve(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSClassID class_id = 0;
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
JS_FreeValue(context, data[0]);
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true;
work->callback(context, true, argv[0], work->user_data);
tf_free(work);
return JS_UNDEFINED;
}
static JSValue _tf_ssb_permission_test_reject(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSClassID class_id = 0;
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
JS_FreeValue(context, data[0]);
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true;
work->callback(context, false, argv[0], work->user_data);
tf_free(work);
return JS_UNDEFINED;
}
static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
{
permission_test_t* work = JS_GetOpaque(value, s_permission_test_class_id);
if (!work->completed)
{
JSValue arg = JS_ThrowInternalError(work->context, "Permission test incomplete.");
work->callback(work->context, false, arg, work->user_data);
JS_FreeValue(work->context, arg);
}
tf_free(work);
}
static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data)
{
if (!s_permission_test_class_id)
{
JSClassDef def = {
.class_name = "permission_test",
.finalizer = _tf_ssb_permission_test_finalizer,
};
JS_NewClassID(&s_permission_test_class_id);
JS_NewClass(JS_GetRuntime(context), s_permission_test_class_id, &def);
}
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
*payload = (permission_test_t) {
.context = context,
.callback = callback,
.user_data = user_data,
};
JSValue opaque = JS_NewObject(context);
JSValue opaque = JS_NewObjectClass(context, s_permission_test_class_id);
JS_SetOpaque(opaque, payload);
JSValue imports = JS_GetPropertyStr(context, process, "imports");
JSValue core = JS_GetPropertyStr(context, imports, "core");
@@ -888,10 +957,11 @@ static void _tf_ssb_permission_test(JSContext* context, JSValue process, const c
JS_FreeValue(context, result);
result = JS_Call(context, catch, promise, 1, &reject);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, opaque);
JS_FreeValue(context, promise);
JS_FreeValue(context, resolve);
JS_FreeValue(context, reject);
JS_FreeValue(context, result);
JS_FreeValue(context, then);
JS_FreeValue(context, catch);
for (int i = 0; i < tf_countof(args); i++)
@@ -1531,6 +1601,133 @@ static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst
return result;
}
typedef struct _append_message_t
{
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char previous_id[512];
int32_t previous_sequence;
JSContext* context;
JSValue promise[2];
JSValue message;
char user[];
} append_message_t;
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
{
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(async->context, error);
JS_FreeValue(async->context, error);
JS_FreeValue(async->context, async->message);
JS_FreeValue(async->context, async->promise[0]);
JS_FreeValue(async->context, async->promise[1]);
tf_free(async);
}
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
{
append_message_t* async = user_data;
JSValue result = JS_UNDEFINED;
if (verified)
{
result = is_new ? JS_TRUE : JS_FALSE;
}
_tf_ssb_appendMessage_finish(async, verified, result);
}
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
{
append_message_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
append_message_t* work = user_data;
if (work->got_private_key)
{
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
JS_FreeValue(work->context, signed_message);
}
else
{
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
}
}
static void _tf_ssb_append_message_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
append_message_t* work = user_data;
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
}
else
{
_tf_ssb_appendMessage_finish(work, false, value);
}
}
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
char description[1024] = "Publish a new message.";
const char* type = tf_util_get_property_as_string(context, argv[1], "type");
if (type)
{
if (strcmp(type, "vote") == 0)
{
JSValue vote = JS_GetPropertyStr(context, argv[1], "vote");
const char* expression = tf_util_get_property_as_string(context, vote, "expression");
snprintf(description, sizeof(description), "React with %s.", expression);
JS_FreeCString(context, expression);
JS_FreeValue(context, vote);
}
else
{
snprintf(description, sizeof(description), "Publish a new %s message.", type);
}
}
else if (JS_IsString(argv[1]))
{
tf_string_set(description, sizeof(description), "Publish a new private message.");
}
JS_FreeCString(context, type);
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
if (!user)
{
return JS_ThrowInternalError(context, "Invalid user.");
}
else
{
size_t user_length = strlen(user);
const char* id = JS_ToCString(context, argv[0]);
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[1]) };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_append", description, _tf_ssb_append_message_permission_callback, work);
return result;
}
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -1544,6 +1741,7 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, core, "permissionsForUser", JS_NewCFunctionData(context, _tf_api_core_permissionsForUser, 1, 0, 1, &process));
JS_SetPropertyStr(context, core, "permissionsGranted", JS_NewCFunctionData(context, _tf_api_core_permissionsGranted, 0, 0, 1, &process));
JS_SetPropertyStr(context, core, "allPermissionsGranted", JS_NewCFunctionData(context, _tf_api_core_allPermissionsGranted, 0, 0, 1, &process));
JSValue app = JS_NewObject(context);
JS_SetPropertyStr(context, app, "owner", JS_GetPropertyStr(context, process, "packageOwner"));
@@ -1559,6 +1757,7 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, ssb, "getOwnerIdentities", JS_NewCFunctionData(context, _tf_ssb_getOwnerIdentities, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "privateMessageEncrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_encrypt, 3, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "privateMessageDecrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_decrypt, 2, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "appendMessageWithIdentity", JS_NewCFunctionData(context, _tf_ssb_appendMessageWithIdentity, 2, 0, 1, &process));
JS_FreeValue(context, ssb);
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");

View File

@@ -308,8 +308,7 @@ static JSValue _httpd_app_on_tfrpc(JSContext* context, JSValueConst this_val, in
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
JSValue process_app = JS_GetPropertyStr(context, app->process, "app");
JSValue calls = JS_IsObject(process_app) ? JS_GetPropertyStr(context, process_app, "calls") : JS_UNDEFINED;
JSValue calls = JS_IsObject(app->process) ? JS_GetPropertyStr(context, app->process, "_calls") : JS_UNDEFINED;
JSValue call = JS_IsObject(calls) ? JS_GetPropertyStr(context, calls, id) : JS_UNDEFINED;
if (!JS_IsUndefined(call))
{
@@ -336,7 +335,6 @@ static JSValue _httpd_app_on_tfrpc(JSContext* context, JSValueConst this_val, in
}
JS_FreeValue(context, call);
JS_FreeValue(context, calls);
JS_FreeValue(context, process_app);
}
JS_FreeCString(context, id);
return JS_UNDEFINED;
@@ -364,13 +362,11 @@ static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this
JS_SetPropertyStr(context, client_api, "tfrpc", tfrpc);
JS_FreeValue(context, client_api);
JSValue process_app = JS_GetPropertyStr(context, app->process, "app");
JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
JS_SetPropertyStr(context, process_app, "_on_output", on_output);
JS_SetPropertyStr(context, app->process, "_on_output", on_output);
JSValue send = JS_GetPropertyStr(context, process_app, "send");
JSValue result = JS_Call(context, send, process_app, 0, NULL);
JS_FreeValue(context, process_app);
JSValue send = JS_GetPropertyStr(context, app->process, "send");
JSValue result = JS_Call(context, send, app->process, 0, NULL);
JS_FreeValue(context, send);
tf_util_report_error(context, result);
JS_FreeValue(context, result);

View File

@@ -1,5 +1,6 @@
#include "serialize.h"
#include "log.h"
#include "mem.h"
#include "task.h"
#include "taskstub.js.h"
@@ -413,7 +414,7 @@ static JSValue _serialize_loadInternal(tf_task_t* task, tf_taskstub_t* from, con
case kObject:
{
int32_t length = _serialize_readInt32(buffer, size);
result = JS_NewObject(context);
result = type == kError ? JS_NewError(context) : JS_NewObject(context);
for (int i = 0; i < length; ++i)
{
JSValue key = _serialize_loadInternal(task, from, buffer, size, depth + 1);

View File

@@ -2838,6 +2838,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
ssb->broadcasts_count--;
tf_free(broadcast);
}
uv_mutex_lock(&ssb->db_readers_lock);
for (int i = 0; i < ssb->db_readers_count; i++)
{
int r = sqlite3_close(ssb->db_readers[i]);
@@ -2846,6 +2847,14 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_printf("sqlite3_close: %s\n", sqlite3_errstr(r));
}
}
if (ssb->db_readers)
{
tf_free(ssb->db_readers);
ssb->db_readers = NULL;
}
ssb->db_readers_count = 0;
uv_mutex_unlock(&ssb->db_readers_lock);
uv_mutex_lock(&ssb->db_writer_lock);
if (ssb->db_writer)
{
int r = sqlite3_close(ssb->db_writer);
@@ -2855,12 +2864,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
}
ssb->db_writer = NULL;
}
ssb->db_readers_count = 0;
if (ssb->db_readers)
{
tf_free(ssb->db_readers);
ssb->db_readers = NULL;
}
uv_mutex_unlock(&ssb->db_writer_lock);
if (ssb->db_path)
{
tf_free((void*)ssb->db_path);

View File

@@ -2,6 +2,7 @@
#include "log.h"
#include "mem.h"
#include "ssb.ebt.h"
#include "ssb.h"
#include "trace.h"
#include "util.js.h"
@@ -440,11 +441,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_id_idx ON blob_wants_cache (id)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_timestamp_id_idx ON blob_wants_cache (timestamp, id)");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_blob_wants_cache");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_blob_wants_cache AFTER INSERT ON messages_refs BEGIN "
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blobs ON new.ref = blobs.id "
"LEFT OUTER JOIN blobs ON new.ref = blobs.id "
"WHERE messages.id = new.message AND "
"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' AND "
"blobs.content IS NULL "
@@ -455,7 +457,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blob_wants_cache bwc ON bwc.source = messages.id AND bwc.id = new.blob "
"JOIN blobs ON bwc.id = blobs.id "
"LEFT OUTER JOIN blobs ON bwc.id = blobs.id "
"WHERE blobs.content IS NULL "
"ON CONFLICT (source, id) DO NOTHING; END");
_tf_ssb_db_exec(db,
@@ -534,46 +536,36 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
return exists;
}
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp, const char* content,
size_t content_len, const char* signature, int flags)
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, sqlite3_stmt* statement, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp,
const char* content, size_t content_len, const char* signature, int flags)
{
int64_t last_row_id = -1;
bool id_mismatch = false;
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
{
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING";
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{
int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
}
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
{
last_row_id = sqlite3_last_insert_rowid(db);
}
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
}
else
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
{
tf_printf("bind failed\n");
last_row_id = sqlite3_last_insert_rowid(db);
}
sqlite3_finalize(statement);
}
else
{
tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
}
sqlite3_reset(statement);
}
else if (id_mismatch)
{
@@ -665,16 +657,27 @@ static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
bool in_transaction = _tf_ssb_db_try_exec(db, "BEGIN TRANSACTION") == SQLITE_OK;
while (store)
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING";
sqlite3_stmt* insert_statement = NULL;
if (sqlite3_prepare_v2(db, query, -1, &insert_statement, NULL) == SQLITE_OK)
{
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp,
store->content, store->length, store->signature, store->flags);
if (last_row_id != -1)
while (store)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, insert_statement, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence,
store->timestamp, store->content, store->length, store->signature, store->flags);
if (last_row_id != -1)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
}
store = store->next;
}
store = store->next;
sqlite3_finalize(insert_statement);
}
else
{
tf_printf("prepare failed: %s.\n", sqlite3_errmsg(db));
}
if (in_transaction)
@@ -760,6 +763,22 @@ static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void*
store = store->next;
}
tf_ssb_connection_t* connections[256];
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
store = user_data;
while (store)
{
for (int i = 0; i < count; i++)
{
tf_ssb_connection_t* connection = connections[i];
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
tf_ssb_ebt_set_messages_received(tf_ssb_connection_get_ebt(connections[i]), store->author, store->sequence);
}
}
store = store->next;
}
if (last_stored)
{
tf_trace_begin(trace, "notify_message_added");

View File

@@ -18,8 +18,6 @@ static const int k_sql_async_timeout_ms = 60 * 1000;
static JSClassID _tf_ssb_classId;
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
typedef struct _create_identity_t
{
char id[k_id_base64_len];
@@ -390,92 +388,6 @@ static JSValue _tf_ssb_getIdentityInfo(JSContext* context, JSValueConst this_val
return result;
}
typedef struct _append_message_t
{
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char previous_id[512];
int32_t previous_sequence;
JSContext* context;
JSValue promise[2];
JSValue message;
char user[];
} append_message_t;
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
{
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(async->context, error);
JS_FreeValue(async->context, error);
JS_FreeValue(async->context, async->message);
JS_FreeValue(async->context, async->promise[0]);
JS_FreeValue(async->context, async->promise[1]);
tf_free(async);
}
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
{
append_message_t* async = user_data;
JSValue result = JS_UNDEFINED;
if (verified)
{
result = is_new ? JS_TRUE : JS_FALSE;
}
_tf_ssb_appendMessage_finish(async, verified, result);
}
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
{
append_message_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
append_message_t* work = user_data;
if (work->got_private_key)
{
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
JS_FreeValue(work->context, signed_message);
}
else
{
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
}
}
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (!ssb)
{
return JS_ThrowInternalError(context, "No SSB instance.");
}
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[2]) };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JS_FreeCString(context, id);
JS_FreeCString(context, user);
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
return result;
}
typedef struct _blob_get_t
{
JSContext* context;
@@ -923,13 +835,14 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
sql_work_t* sql_work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
uint8_t* p = sql_work->rows;
while (p < sql_work->rows + sql_work->rows_count)
JSValue result = JS_UNDEFINED;
while (p < sql_work->rows + sql_work->rows_count && JS_IsUndefined(result))
{
if (*p++ == 'r')
{
JSValue row = JS_NewObject(context);
while (*p == 'c')
while (*p == 'c' && JS_IsUndefined(result))
{
p++;
const char* column_name = (const char*)p;
@@ -970,14 +883,13 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
}
}
JSValue response = JS_Call(context, sql_work->callback, JS_UNDEFINED, 1, &row);
bool is_error = tf_util_report_error(context, response);
JS_FreeValue(context, response);
JS_FreeValue(context, row);
if (is_error)
result = JS_Call(context, sql_work->callback, JS_UNDEFINED, 1, &row);
if (!JS_IsException(result))
{
break;
JS_FreeValue(context, result);
result = JS_UNDEFINED;
}
JS_FreeValue(context, row);
}
else
{
@@ -985,8 +897,20 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
}
}
JSValue result = JS_UNDEFINED;
if (sql_work->result == SQLITE_OK || sql_work->result == SQLITE_DONE)
if (!JS_IsUndefined(result))
{
bool is_exception = JS_IsException(result);
if (is_exception)
{
JSValue exception = JS_GetException(context);
JS_FreeValue(context, result);
result = exception;
}
JSValue promise_result = JS_Call(context, sql_work->promise[is_exception ? 1 : 0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, promise_result);
JS_FreeValue(context, promise_result);
}
else if (sql_work->result == SQLITE_OK || sql_work->result == SQLITE_DONE)
{
result = JS_Call(context, sql_work->promise[0], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, result);
@@ -1783,8 +1707,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
JS_SetPropertyStr(context, object, "setUserPermission", JS_NewCFunction(context, _tf_ssb_set_user_permission, "setUserPermission", 5));
/* Write. */
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
/* Does not require an identity. */
JS_SetPropertyStr(context, object, "getServerIdentity", JS_NewCFunction(context, _tf_ssb_getServerIdentity, "getServerIdentity", 0));

View File

@@ -190,15 +190,31 @@ static void _tf_ssb_rpc_blobs_has(tf_ssb_connection_t* connection, uint8_t flags
JS_FreeValue(context, ids);
}
static bool _tf_ssb_rpc_are_messages_pending_in(tf_ssb_connection_t* connection)
{
int in_pending = 0;
int in_total = 0;
int out_pending = 0;
int out_total = 0;
if (connection)
{
tf_ssb_ebt_get_progress(tf_ssb_connection_get_ebt(connection), &in_pending, &in_total, &out_pending, &out_total);
}
return in_pending != 0;
}
static void _tf_ssb_rpc_blob_wants_added_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{
tf_ssb_connection_t* connection = user_data;
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, id, JS_NewInt64(context, -1));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
if (!_tf_ssb_rpc_are_messages_pending_in(connection))
{
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, id, JS_NewInt64(context, -1));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
}
typedef struct _blob_wants_work_t
@@ -235,7 +251,8 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
if (sqlite3_bind_text(statement, 1, blob_wants->last_id, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, timestamp) == SQLITE_OK &&
sqlite3_bind_int(statement, 3, tf_countof(work->out_id)) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
int r = SQLITE_OK;
while ((r = sqlite3_step(statement)) == SQLITE_ROW)
{
tf_string_set(work->out_id[work->out_id_count], sizeof(work->out_id[work->out_id_count]), (const char*)sqlite3_column_text(statement, 0));
work->out_id_count++;
@@ -264,7 +281,10 @@ static void _tf_ssb_request_blob_wants_after_work(tf_ssb_connection_t* connectio
JS_SetPropertyStr(context, message, work->out_id[i], JS_NewInt32(context, -1));
send_failed = !tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
blob_wants->wants_sent++;
if (!send_failed)
{
blob_wants->wants_sent++;
}
}
if (work->out_id_count)
{
@@ -276,9 +296,12 @@ static void _tf_ssb_request_blob_wants_after_work(tf_ssb_connection_t* connectio
static void _tf_ssb_rpc_request_more_blobs(tf_ssb_connection_t* connection)
{
blob_wants_work_t* work = tf_malloc(sizeof(blob_wants_work_t));
memset(work, 0, sizeof(*work));
tf_ssb_connection_run_work(connection, _tf_ssb_request_blob_wants_work, _tf_ssb_request_blob_wants_after_work, work);
if (tf_ssb_connection_get_blob_wants_state(connection)->wants_sent == 0 && !_tf_ssb_rpc_are_messages_pending_in(connection))
{
blob_wants_work_t* work = tf_malloc(sizeof(blob_wants_work_t));
memset(work, 0, sizeof(*work));
tf_ssb_connection_run_work(connection, _tf_ssb_request_blob_wants_work, _tf_ssb_request_blob_wants_after_work, work);
}
}
static void _tf_ssb_rpc_blobs_createWants(
@@ -718,10 +741,12 @@ static void _tf_ssb_rpc_connection_blobs_createWants_callback(
}
int64_t size = 0;
JS_ToInt64(context, &size, key_value);
if (--blob_wants->wants_sent == 0)
/* We don't have the context here to disambiguate responses to incoming requests. */
if (blob_wants->wants_sent > 0)
{
_tf_ssb_rpc_request_more_blobs(connection);
--blob_wants->wants_sent;
}
_tf_ssb_rpc_request_more_blobs(connection);
if (size < 0)
{
blob_create_wants_work_t* work = tf_malloc(sizeof(blob_create_wants_work_t));
@@ -1940,8 +1965,8 @@ static void _tf_ssb_rpc_message_added_callback(tf_ssb_t* ssb, const char* author
tf_ssb_connection_t* connection = connections[i];
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
tf_ssb_ebt_set_messages_received(tf_ssb_connection_get_ebt(connections[i]), author, sequence);
_tf_ssb_rpc_ebt_schedule_send_clock(connections[i]);
_tf_ssb_rpc_request_more_blobs(connection);
}
}
}

View File

@@ -81,11 +81,11 @@ try:
select(driver, ['#document', 'frame', '=identity'])
driver.get('http://localhost:8888/')
select(driver, ['#document', 'frame', '//button[text()="Next"]'], ('click',))
select(driver, ['#document', 'frame', '//button[text()="Onward"]'], ('click',))
select(driver, ['#document', 'frame', '//button[text()="Got It"]'], ('click',))
select(driver, ['#document', 'frame', '//button[text()="Okay"]'], ('click',))
select(driver, ['#document', 'frame', '//button[text()="Let\'s Go!"]'], ('click',))
select(driver, ['#document', 'frame', '#next0'], ('click',))
select(driver, ['#document', 'frame', '#next1'], ('click',))
select(driver, ['#document', 'frame', '#next2'], ('click',))
select(driver, ['#document', 'frame', '#next3'], ('click',))
select(driver, ['#document', 'frame', '//button[text()="Continue"]'], ('click',))
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'We made it to the ssb app.'))
@@ -129,6 +129,11 @@ try:
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '.tf-profile', 'shadow_root', '#open_private_chat'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'This is a private message.'))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
driver.get('http://localhost:8888/~testuser/test/')
select(driver, ['#document'])
select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))