51 Commits

Author SHA1 Message Date
85bbb9c010 docs: Update build notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 10m13s
2025-12-27 16:30:09 -05:00
82b0fe8d57 build: Actually dist the aarch64 appimage.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-27 16:27:08 -05:00
00b233dd95 core: A little more tracing. 2025-12-27 16:14:30 -05:00
7154212ddd ssb: Make editing your profile look a little more obvious.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 11m27s
2025-12-27 15:55:16 -05:00
364c4c04ac core: loadSettings() no longer needed.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m23s
Build Tilde Friends / Build-All (push) Successful in 10m56s
2025-12-27 14:51:33 -05:00
878c022934 ssb: Attempt to group related messages (bookclub + bookclubUpdates, wiki + wiki-doc, ...) 2025-12-27 14:35:58 -05:00
2c9654b480 android: Fix copying message ids based on: https://stackoverflow.com/questions/61401384/can-text-within-an-iframe-be-copied-to-clipboard.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 12m21s
2025-12-27 11:55:29 -05:00
14e36308f9 ssb: Fix the messages_stats trigger.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m41s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-27 11:24:09 -05:00
cd8df2fe15 welcome: Routine re-wordsmithing.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m47s
Build Tilde Friends / Build-All (push) Successful in 10m36s
2025-12-27 11:04:48 -05:00
8abcdd1e7d core: Fix unauthenticated sessions.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m38s
Build Tilde Friends / Build-All (push) Successful in 9m52s
2025-12-26 21:53:54 -05:00
97aeff60cc build: Build an aarch64 .AppImage.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m28s
Build Tilde Friends / Build-All (push) Successful in 10m6s
2025-12-26 21:40:11 -05:00
86d6a5c049 update: CodeMirror. 2025-12-26 21:24:52 -05:00
f6f815eec1 ssb: Fix the oblong spinning refresh button. 2025-12-26 21:22:00 -05:00
73a1c1d978 core: Move ssb.getPrivateKey from JS => C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-12-26 17:27:36 -05:00
5445072d36 core: Move ssb.deleteIdentity from JS => C. 2025-12-26 17:07:07 -05:00
d9a2519e9b core: Move ssb.addItentity() from JS => C. 2025-12-26 16:48:46 -05:00
687a85dbd8 build: Disable -t auto again. Oh well.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Successful in 10m26s
2025-12-26 16:16:33 -05:00
9e25aa1c54 build: Maybe on trixie?
Some checks failed
Build Tilde Friends / Build-Docs (push) Failing after 4m41s
Build Tilde Friends / Build-All (push) Successful in 11m3s
2025-12-26 10:13:08 -05:00
e309f519f2 build: Oops, actually test the thing.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Failing after 10m55s
2025-12-25 11:18:46 -05:00
5ccd9f16c3 build: Last try.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-25 11:14:10 -05:00
7d596ebd3b build: Maybe like this?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Failing after 6s
2025-12-25 11:10:01 -05:00
938f728eb9 build: Just curious, can the CI worker run headless selenium tests?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 12s
2025-12-25 11:03:22 -05:00
6e8a0031a8 bookclub: Handle both about and bookclubUpdate messages. 2025-12-25 10:38:18 -05:00
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
40 changed files with 1630 additions and 1060 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
@@ -1213,7 +1213,26 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
out/tildefriends-aarch64.AppImage: out/armrelease/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends_aarch64.AppDir
@mkdir -p out/tildefriends_aarch64.AppDir/usr/bin
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/applications
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends_aarch64.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/
@cp out/armrelease/tildefriends out/tildefriends_aarch64.AppDir/usr/bin/
@cp out/data.zip out/tildefriends_aarch64.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends_aarch64.AppDir/AppRun
@chmod +x out/tildefriends_aarch64.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=arm_aarch64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-aarch64.AppImage.zsync' tildefriends_aarch64.AppDir tildefriends-aarch64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage out/tildefriends-aarch64.AppImage ## Build AppImages.
.PHONY: appimage
flatpak: out/ ## Build a flatpak.
@@ -1300,6 +1319,8 @@ dist:
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
@echo "[cp] TildeFriends-aarch64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-aarch64.AppImage dist/TildeFriends-aarch64-$(VERSION_NUMBER).AppImage
@echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)
@test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📚",
"previous": "&yLHlvKirJEqrekP5lf5BydvzIo/vN+z7K2ACQacxJXE=.sha256"
"emoji": "📖",
"previous": "&u7ri5Gi1AK6SbWRmc3S8vN40QrWL90/DKDiDTeDDiPQ=.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,79 @@ 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(?1) AS book, json_each(?2) 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')
UNION
SELECT
messages.author,
messages.content ->> '$.updates' AS about,
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.updates', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?1) AS book, json_each(?2) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'bookclubUpdate'
AND messages.content ->> '$.updates' = book.value
AND NOT fields.key IN ('about', 'updates', '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": "&PrgfYYKpL2tedApJXokIIk+h9/yRYo0aUliNF3QsnZ4=.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;
@@ -829,16 +866,20 @@ class TfElement extends LitElement {
this.is_administrator
? html`
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple w3-right' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
class="w3-bar-item w3-button w3-circle w3-right"
@click=${this.refresh}
>
<span
style="display: inline-block"
class=${this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: ''}
>
</span>
</button>
<button
class="w3-bar-item w3-button w3-ripple w3-right"
class="w3-bar-item w3-button w3-right"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}
@@ -889,6 +930,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

@@ -2,6 +2,7 @@ import {
LitElement,
css,
html,
map,
repeat,
render,
unsafeCSS,
@@ -472,7 +473,9 @@ class TfMessageElement extends LitElement {
}
copy_id(event) {
navigator.clipboard.writeText(this.message?.id);
navigator.clipboard.writeText(this.message?.id).catch(function (e) {
console.log(e);
});
}
toggle_menu(event) {
@@ -595,6 +598,31 @@ class TfMessageElement extends LitElement {
`;
}
render_refs() {
if (this.message?.refs) {
let self = this;
return html`<div class="w3-container">
<h3>Referring messages</h3>
${map(
this.message.refs,
(ref) => html`
<tf-message
.message=${ref}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
.recent_reactions=${self.recent_reactions}
depth=${self.depth + 1}
></tf-message>
`
)}
</div>`;
}
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
@@ -604,7 +632,7 @@ class TfMessageElement extends LitElement {
? html`${self.render_raw()}`
: self.render_flagged(inner)}
</div>
${self.render_votes()}
${self.render_votes()} ${self.render_refs()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -914,6 +942,7 @@ class TfMessageElement extends LitElement {
</div>
<div class="w3-container">${this.render_flagged(undefined)}</div>
<div>${this.render_votes()}</div>
${this.render_refs()}
${(this.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -980,7 +1009,7 @@ class TfMessageElement extends LitElement {
></tf-user>
</div>
${this.render_menu()} ${this.render_votes()}
${this.render_actions()}
${this.render_refs()} ${this.render_actions()}
</div>
`);
break;
@@ -988,7 +1017,9 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
break;
@@ -1051,12 +1082,15 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${payload}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
} else if (content.type === 'issue') {
return this.render_frame(html`
${this.render_header()} ${content.text} ${this.render_votes()}
${this.render_refs()}
<footer class="w3-container">
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
@@ -1109,7 +1143,7 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div>${body}</div>
${this.render_mentions()} ${this.render_votes()}
${this.render_mentions()} ${this.render_votes()} ${this.render_refs()}
${this.render_actions()}
`);
} else if (content.type === 'pub') {
@@ -1160,13 +1194,21 @@ class TfMessageElement extends LitElement {
}
} else {
return this.render_small_frame(
html`<div class="w3-container">
<p><b>type</b>: ${content.type}</p>
</div>`
html`<p><b>type</b>: ${content.type}</p>`
);
}
} 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

@@ -95,6 +95,22 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.root[0];
}
}
} else {
let parent_id = message.content.about?.startswith?.('%')
? message.content.about
: message.content.updates?.startsWith?.('%')
? message.content.updates
: message.content.parent?.startsWith?.('%')
? message.content.parent
: undefined;
if (parent_id) {
let parent = ensure_message(parent_id, message.rowid);
if (!parent?.refs) {
parent.refs = [];
}
parent.refs.push(message);
message.parent_message = parent_id;
}
}
}

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;
@@ -260,16 +274,18 @@ class TfProfileElement extends LitElement {
if (this.id === this.whoami) {
if (this.editing) {
edit = html`
<button
id="save_profile"
class="w3-button w3-theme-d1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
<div style="margin-top: 8px">
<button
id="save_profile"
class="w3-button w3-theme-l1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
</div>
`;
} else {
edit = html`<button
@@ -315,7 +331,7 @@ class TfProfileElement extends LitElement {
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>`
</div>`
: null;
let image = profile.image;
if (typeof image == 'string' && !image.startsWith('&')) {
@@ -327,7 +343,7 @@ class TfProfileElement extends LitElement {
let description = this.editing?.description ?? profile.description;
return html`
<style>${generate_theme()}</style>
<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<div class="w3-card-4 w3-theme-d3" style="box-sizing: border-box">
<header class="w3-container">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
</header>
@@ -336,19 +352,23 @@ class TfProfileElement extends LitElement {
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
<div class=${this.editing ? 'w3-card' : ''}>
${this.editing ? html`<header class="w3-container w3-theme-l2"><h2>Editing Your Profile</h2></header>` : undefined}
<div style="display: flex; flex-direction: row; gap: 1em" class="w3-margin">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: min(256px, 100%); height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
${this.editing ? html`<footer class="w3-container w3-theme-l2"><p>${edit}</p></footer>` : undefined}
</div>
<div>
Following ${profile.following} identities.
@@ -360,10 +380,10 @@ 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>
${edit}
</button>
${this.editing ? undefined : edit}
${follow}
${block}
</p>

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": "&sVSmI40DUgnS4TUa2AiKrlNj+qN3WDeXII3364OSMIo=.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>
@@ -268,11 +268,11 @@
</div>
<!-- Sandbox Section -->
<div class="w3-padding-64 w3-grey">
<div class="w3-padding-64 w3-pale-blue">
<div class="w3-row-padding">
<div class="w3-col">
<h1 class="w3-jumbo" style="text-align: right">
<b>Sandbox Security</b>
<b>App Sandboxes</b>
</h1>
<i
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
@@ -280,7 +280,7 @@
></i>
<p>
Tilde Friends tries to make sure apps can be trusted using similar
techniques to how web browsers and operating systems do it.
techniques to web browsers and operating systems.
</p>
<p>
This is all a work in progress, and it varies by platform, so don't
@@ -294,16 +294,24 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Built to Last</b></h1>
<p>
Tilde Friends strives to use only simple and widely adopted dependencies
in order to keep it easy to build for all sorts of platforms and
maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits on top.
</p>
<h1 class="w3-jumbo"><b>One Pile of Code</b></h1>
<div class="w3-left-align">
<p>
Tilde Friends diverges from the Node.js web of modules from which
Secure Scuttlebutt was first developed. Here we strive to maintain a
single C program that works as a foundation for building a wide
variety of social and other applications.
</p>
<p>
Tilde Friends uses only a handful of simple and widely adopted
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever works.
</p>
</div>
<div class="w3-row" style="margin-top: 64px">
<a
@@ -373,7 +381,7 @@
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
<p class="w3-medium">
This page and Tilde Friends itself was made by Cory mostly in coffee
shops and a local pizza place.
shops and a local pizza place while listening to Gary's Bangers.
</p>
</footer>
</body>

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,34 @@ 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 allow = iframe.allow;
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;
new_iframe.allow = allow;
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 +1148,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 +1527,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 +1927,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,46 +229,17 @@ 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();
if (!user || !options?.packageOwner || !options?.packageName) {
return;
} else if (
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission] !== undefined
) {
if (
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission]
) {
let permissions = await imports.core.permissionsGranted();
if (permissions && permissions[permission] !== undefined) {
if (permissions[permission]) {
return true;
} 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 +269,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
}
throw Error(`Permission denied: ${permission}.`);
});
} else {
throw Error(`Permission denied: ${permission}.`);
}
},
},
@@ -331,7 +281,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 +343,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,88 +354,12 @@ 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)])
);
imports.ssb.createIdentity = () => process.createIdentity();
imports.ssb.addIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_add')
).then(function () {
return ssb.addIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.deleteIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_delete')
).then(function () {
return ssb.deleteIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getPrivateKey = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_export')
).then(function () {
return ssb.getPrivateKey(process.credentials.session.name, id);
});
}
};
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 +409,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 +440,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 +470,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;
@@ -647,39 +517,15 @@ ssb_internal.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []);
});
/**
* Load settings from the database.
* @return The settings as a key value pairs object.
*/
async function loadSettings() {
let data = {};
try {
let settings = await new Database('core').get('settings');
if (settings) {
data = JSON.parse(settings);
}
} catch (error) {
print('Settings not found in database:', error);
}
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
}
return data;
}
/**
* 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,7 @@
<iframe
id="document"
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
style="width: 100%; height: 100%; border: 0"
allow="clipboard-write"
></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.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.7.tgz",
"integrity": "sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==",
"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,7 @@
* First launch after update may be noticeably slower due to database reindexing.
* Crash fixes.
* Fix channels with hyphens and various other characters not working correctly.
* Navigation bar, search UI, private message, and profile 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

@@ -5,6 +5,7 @@
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "trace.h"
#include "util.js.h"
#include "quickjs.h"
@@ -24,6 +25,8 @@
#include <alloca.h>
#endif
static JSClassID s_permission_test_class_id;
typedef struct _app_path_pair_t
{
const char* app;
@@ -417,6 +420,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 +887,76 @@ 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;
char name[];
} 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;
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(context, true, argv[0], work->user_data);
tf_free(work);
tf_trace_end(trace);
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;
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(context, false, argv[0], work->user_data);
tf_free(work);
tf_trace_end(trace);
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.");
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(work->context, false, arg, work->user_data);
tf_trace_end(trace);
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)
{
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
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);
}
const char* name = tf_util_function_to_string(callback);
size_t name_length = name ? strlen(name) : 0;
permission_test_t* payload = tf_malloc(sizeof(permission_test_t) + name_length + 1);
*payload = (permission_test_t) {
.context = context,
.callback = callback,
.user_data = user_data,
};
JSValue opaque = JS_NewObject(context);
tf_string_set(payload->name, name_length + 1, name);
tf_free((void*)name);
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 +975,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 +1619,388 @@ 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;
}
}
typedef struct _add_identity_t
{
uint8_t key[crypto_sign_SECRETKEYBYTES / 2];
bool added;
JSValue result;
JSValue promise[2];
char user[];
} add_identity_t;
static void _tf_ssb_add_identity_work(tf_ssb_t* ssb, void* user_data)
{
add_identity_t* work = user_data;
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
unsigned char seed[crypto_sign_SEEDBYTES];
uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
memcpy(secret_key, work->key, sizeof(secret_key) / 2);
if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
{
char public_key_b64[512];
tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
snprintf(public_key_b64 + strlen(public_key_b64), sizeof(public_key_b64) - strlen(public_key_b64), ".ed25519");
uint8_t combined[crypto_sign_SECRETKEYBYTES];
memcpy(combined, work->key, sizeof(work->key));
memcpy(combined + sizeof(work->key), public_key, sizeof(public_key));
char combined_b64[512];
tf_base64_encode(combined, sizeof(combined), combined_b64, sizeof(combined_b64));
snprintf(combined_b64 + strlen(combined_b64), sizeof(combined_b64) - strlen(combined_b64), ".ed25519");
work->added = tf_ssb_db_identity_add(ssb, work->user, public_key_b64, combined_b64);
}
}
static void _tf_ssb_add_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
add_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_IsUndefined(work->result) ? (work->added ? JS_TRUE : JS_UNDEFINED) : work->result;
JSValue error = JS_Call(context, work->promise[work->added ? 0 : 1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static void _tf_ssb_add_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
add_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_add_identity_work, _tf_ssb_add_identity_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_add_identity_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_addIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
JSValue result = JS_UNDEFINED;
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
size_t user_length = user ? strlen(user) : 0;
JSValue buffer = JS_UNDEFINED;
size_t length = 0;
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[0]);
if (!array)
{
size_t offset;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
if (!JS_IsException(buffer))
{
array = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
if (array)
{
if (length == crypto_sign_SECRETKEYBYTES / 2)
{
add_identity_t* work = tf_malloc(sizeof(add_identity_t) + user_length + 1);
*work = (add_identity_t) { .result = JS_UNDEFINED };
memcpy(work->key, array, sizeof(work->key));
if (user)
{
memcpy(work->user, user, user_length + 1);
}
result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_add", "Add an identity.", _tf_ssb_add_identity_permission_callback, work);
}
else
{
result = JS_ThrowInternalError(context, "Unexpected private key size: %d vs. %d\n", (int)length, crypto_sign_SECRETKEYBYTES);
}
}
else
{
result = JS_ThrowInternalError(context, "Expected array argument.");
}
JS_FreeValue(context, buffer);
JS_FreeCString(context, user);
return result;
}
typedef struct _delete_identity_t
{
char id[k_id_base64_len];
bool deleted;
JSValue result;
JSValue promise[2];
char user[];
} delete_identity_t;
static void _tf_ssb_delete_identity_work(tf_ssb_t* ssb, void* user_data)
{
delete_identity_t* work = user_data;
work->deleted = tf_ssb_db_identity_delete(ssb, work->user, work->id);
}
static void _tf_ssb_delete_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->deleted ? JS_TRUE : JS_FALSE;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static void _tf_ssb_delete_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
delete_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_delete_identity_work, _tf_ssb_delete_identity_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_delete_identity_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue process = data[0];
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
const char* id = JS_ToCString(context, argv[0]);
if (id && user)
{
size_t user_length = strlen(user);
delete_identity_t* work = tf_malloc(sizeof(delete_identity_t) + user_length + 1);
*work = (delete_identity_t) { .result = JS_UNDEFINED };
tf_string_set(work->id, sizeof(work->id), *id == '@' ? id + 1 : id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_delete", "Delete an identity.", _tf_ssb_delete_identity_permission_callback, work);
}
JS_FreeCString(context, id);
JS_FreeCString(context, user);
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
JSValue result;
JSValue promise[2];
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char user[];
} get_private_key_t;
static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
get_private_key_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));
}
}
static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_private_key_t* work = user_data;
JSValue result = JS_UNDEFINED;
JSContext* context = work->context;
if (work->got_private_key && JS_IsUndefined(work->result))
{
result = tf_util_new_uint8_array(context, work->private_key, sizeof(work->private_key) / 2);
}
JSValue error = JS_Call(context, work->promise[work->got_private_key ? 0 : 1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->result);
tf_free(work);
}
static void _tf_ssb_get_private_key_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
get_private_key_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_get_private_key_work, _tf_ssb_get_private_key_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_get_private_key_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_getPrivateKey(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
size_t user_length = user ? strlen(user) : 0;
const char* id = JS_ToCString(context, argv[0]);
get_private_key_t* work = tf_malloc(sizeof(get_private_key_t) + user_length + 1);
*work = (get_private_key_t) { .context = context, .result = JS_UNDEFINED };
if (user)
{
memcpy(work->user, user, user_length + 1);
}
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_export", "Export a private key.", _tf_ssb_get_private_key_permission_callback, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -1544,6 +2014,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 +2030,10 @@ 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_SetPropertyStr(context, ssb, "addIdentity", JS_NewCFunctionData(context, _tf_ssb_addIdentity, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "deleteIdentity", JS_NewCFunctionData(context, _tf_ssb_deleteIdentity, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getPrivateKey", JS_NewCFunctionData(context, _tf_ssb_getPrivateKey, 1, 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);
@@ -675,7 +671,7 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
JSValue user_permissions = name_string && !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++)
{
@@ -747,7 +743,6 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
tf_free((void*)cookie);
JS_FreeCString(context, name_string);
// tf_http_request_unref(request);
request->on_message = _httpd_app_on_message;
request->on_close = _httpd_app_on_close;
request->context = context;
@@ -773,18 +768,19 @@ void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
tf_free((void*)session);
JSValue credentials = JS_NewObject(context);
if (!JS_IsUndefined(jwt))
{
JSValue credentials = JS_NewObject(context);
JS_SetPropertyStr(context, credentials, "session", jwt);
tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}

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

@@ -173,6 +173,7 @@ typedef struct _tf_ssb_timer_t
uv_timer_t timer;
void (*callback)(tf_ssb_t* ssb, void* user_data);
void* user_data;
char name[];
} tf_ssb_timer_t;
typedef struct _tf_ssb_broadcast_result_t
@@ -2838,6 +2839,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 +2848,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 +2865,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);
@@ -4579,7 +4584,9 @@ void tf_ssb_set_quiet(tf_ssb_t* ssb, bool quiet)
static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
{
tf_ssb_timer_t* timer = handle->data;
tf_trace_begin(timer->ssb->trace, timer->name);
timer->callback(timer->ssb, timer->user_data);
tf_trace_end(timer->ssb->trace);
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
}
@@ -4590,8 +4597,10 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
return;
}
const char* name = tf_util_function_to_string(callback);
size_t name_length = name ? strlen(name) : 0;
ssb->timers = tf_resize_vec(ssb->timers, sizeof(uv_timer_t*) * (ssb->timers_count + 1));
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t));
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t) + name_length + 1);
*timer = (tf_ssb_timer_t)
{
.ssb = ssb,
@@ -4602,6 +4611,8 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
.callback = callback,
.user_data = user_data,
};
tf_string_set(timer->name, name_length + 1, name);
tf_free((void*)name);
ssb->timers[ssb->timers_count++] = timer;
uv_timer_init(ssb->loop, &timer->timer);
uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);

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"
@@ -170,6 +171,10 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
}
}
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_stats' AND NOT sql LIKE '%excluded.size%'"))
{
_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
}
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
{
_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
@@ -185,10 +190,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"messages GROUP BY author");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
}
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_stats");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_stats AFTER INSERT ON messages BEGIN INSERT INTO messages_stats(author, max_sequence, max_timestamp, size) VALUES (new.author, "
"new.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, new.sequence), max_timestamp = MAX(max_timestamp, "
"new.timestamp), size = size + length(json(new.content)); END");
"new.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, excluded.max_sequence), max_timestamp = "
"MAX(max_timestamp, "
"excluded.max_timestamp), size = size + excluded.size; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_stats");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ad_stats AFTER DELETE ON messages BEGIN "
@@ -440,11 +447,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 +463,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 +542,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 +663,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 +769,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];
@@ -98,209 +96,6 @@ static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val,
return result;
}
typedef struct _add_identity_t
{
uint8_t key[crypto_sign_SECRETKEYBYTES / 2];
bool added;
JSValue promise[2];
char user[];
} add_identity_t;
static void _tf_ssb_add_identity_work(tf_ssb_t* ssb, void* user_data)
{
add_identity_t* work = user_data;
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
unsigned char seed[crypto_sign_SEEDBYTES];
uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
memcpy(secret_key, work->key, sizeof(secret_key) / 2);
if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
{
char public_key_b64[512];
tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
snprintf(public_key_b64 + strlen(public_key_b64), sizeof(public_key_b64) - strlen(public_key_b64), ".ed25519");
uint8_t combined[crypto_sign_SECRETKEYBYTES];
memcpy(combined, work->key, sizeof(work->key));
memcpy(combined + sizeof(work->key), public_key, sizeof(public_key));
char combined_b64[512];
tf_base64_encode(combined, sizeof(combined), combined_b64, sizeof(combined_b64));
snprintf(combined_b64 + strlen(combined_b64), sizeof(combined_b64) - strlen(combined_b64), ".ed25519");
work->added = tf_ssb_db_identity_add(ssb, work->user, public_key_b64, combined_b64);
}
}
static void _tf_ssb_add_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
add_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->added ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_addIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
JSValue buffer = JS_UNDEFINED;
size_t length = 0;
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[1]);
if (!array)
{
size_t offset;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[1], &offset, &length, &element_size);
if (!JS_IsException(buffer))
{
array = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
if (array)
{
if (length == crypto_sign_SECRETKEYBYTES / 2)
{
add_identity_t* work = tf_malloc(sizeof(add_identity_t) + user_length + 1);
*work = (add_identity_t) { 0 };
memcpy(work->key, array, sizeof(work->key));
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_add_identity_work, _tf_ssb_add_identity_after_work, work);
}
else
{
result = JS_ThrowInternalError(context, "Unexpected private key size: %d vs. %d\n", (int)length, crypto_sign_SECRETKEYBYTES);
}
}
else
{
result = JS_ThrowInternalError(context, "Expected array argument.");
}
JS_FreeValue(context, buffer);
JS_FreeCString(context, user);
}
return result;
}
typedef struct _delete_identity_t
{
char id[k_id_base64_len];
bool deleted;
JSValue promise[2];
char user[];
} delete_identity_t;
static void _tf_ssb_delete_identity_work(tf_ssb_t* ssb, void* user_data)
{
delete_identity_t* work = user_data;
work->deleted = tf_ssb_db_identity_delete(ssb, work->user, work->id);
}
static void _tf_ssb_delete_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->deleted ? JS_TRUE : JS_FALSE;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
if (id && user)
{
delete_identity_t* work = tf_malloc(sizeof(delete_identity_t) + user_length + 1);
*work = (delete_identity_t) { 0 };
tf_string_set(work->id, sizeof(work->id), *id == '@' ? id + 1 : id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_delete_identity_work, _tf_ssb_delete_identity_after_work, work);
}
JS_FreeCString(context, id);
JS_FreeCString(context, user);
}
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
JSValue promise[2];
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char user[];
} get_private_key_t;
static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
get_private_key_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));
}
}
static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_private_key_t* work = user_data;
JSValue result = JS_UNDEFINED;
JSContext* context = work->context;
if (work->got_private_key)
{
result = tf_util_new_uint8_array(context, work->private_key, sizeof(work->private_key) / 2);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_getPrivateKey(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
get_private_key_t* work = tf_malloc(sizeof(get_private_key_t) + user_length + 1);
*work = (get_private_key_t) { .context = context };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_private_key_work, _tf_ssb_get_private_key_after_work, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
return result;
}
static JSValue _tf_ssb_getServerIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
@@ -390,92 +185,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 +632,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 +680,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 +694,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);
@@ -1779,12 +1500,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
/* Requires an identity. */
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
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

@@ -114,6 +114,21 @@ static int _ssb_test_count_messages(tf_ssb_t* ssb)
return count.count;
}
static void _dump_stats_callback(JSValue row, void* user_data)
{
JSContext* context = tf_ssb_get_context(user_data);
JSValue json = JS_JSONStringify(context, row, JS_UNDEFINED, JS_UNDEFINED);
const char* string = JS_ToCString(context, json);
tf_printf("%s\n", string);
JS_FreeCString(context, string);
JS_FreeValue(context, json);
}
static void _ssb_test_dump_messages_stats(tf_ssb_t* ssb)
{
tf_ssb_db_visit_query(ssb, "SELECT * FROM messages_stats", JS_UNDEFINED, _dump_stats_callback, ssb);
}
static void _message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, void* user_data)
{
++*(int*)user_data;
@@ -248,6 +263,8 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uv_sleep(1000);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
@@ -258,6 +275,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
@@ -273,6 +291,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uint8_t* b0;
size_t s0 = 0;

View File

@@ -67,7 +67,7 @@ success = False
try:
options = webdriver.FirefoxOptions()
service = Service(log_output = 'out/geckodriver.log')
#options.add_argument('--headless')
options.add_argument('--headless')
driver = webdriver.Firefox(options = options, service = service)
wait = WebDriverWait(driver, 10)
@@ -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',))