Compare commits
28 Commits
a5ed64f866
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 085f62aadf | |||
| 71d556143b | |||
| 081bff9a26 | |||
| f2f4455c82 | |||
| 2d38b3bd61 | |||
| 3e73c9e00b | |||
| 9aa0e2eda4 | |||
| 076cc265f8 | |||
| f0ee9808a9 | |||
| 27be6de208 | |||
| 304bb82c74 | |||
| ec80f27434 | |||
| 12733acd9f | |||
| 7ba28a269a | |||
| 28e6004c91 | |||
| 1a1d84e603 | |||
| da1116220c | |||
| 57c945e2cf | |||
| bef66329b2 | |||
| 4f726ce502 | |||
| cc0266b5e8 | |||
| dad14d1754 | |||
| c867204233 | |||
| 68eb53b1ff | |||
| 403c5fcfe6 | |||
| c0ed9fda01 | |||
| 95d263e139 | |||
| 782013f3a3 |
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📚",
|
||||
"previous": "&yLHlvKirJEqrekP5lf5BydvzIo/vN+z7K2ACQacxJXE=.sha256"
|
||||
"previous": "&EO5ifwzemEeSJsN6SJ2VTyE+sqnwU2gikIngQimwnDo=.sha256"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as commonmark from './commonmark.min.js';
|
||||
|
||||
async function query(sql, args) {
|
||||
let result = [];
|
||||
await ssb.sqlAsync(sql, args, function (row) {
|
||||
@@ -8,174 +6,66 @@ async function query(sql, args) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function image(node, entering) {
|
||||
if (
|
||||
node.firstChild?.type === 'text' &&
|
||||
node.firstChild.literal.startsWith('video:')
|
||||
) {
|
||||
if (entering) {
|
||||
this.lit(
|
||||
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||
this.esc(node.firstChild?.literal) +
|
||||
'" controls>'
|
||||
);
|
||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
this.lit('</video>');
|
||||
}
|
||||
} else if (
|
||||
node.firstChild?.type === 'text' &&
|
||||
node.firstChild.literal.startsWith('audio:')
|
||||
) {
|
||||
if (entering) {
|
||||
this.lit(
|
||||
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||
this.esc(node.firstChild?.literal) +
|
||||
'" controls>'
|
||||
);
|
||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
this.lit('</audio>');
|
||||
}
|
||||
} else {
|
||||
if (entering) {
|
||||
if (this.disableTags === 0) {
|
||||
this.lit(
|
||||
'<div class="img_caption">' +
|
||||
this.esc(node.firstChild?.literal || node.destination) +
|
||||
'</div>'
|
||||
);
|
||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||
this.lit('<img src="" title="');
|
||||
} else {
|
||||
this.lit('<img src="' + this.esc(node.destination) + '" title="');
|
||||
}
|
||||
}
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
if (this.disableTags === 0) {
|
||||
if (node.title) {
|
||||
this.lit('" title="' + this.esc(node.title));
|
||||
}
|
||||
this.lit('" />');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function code(node) {
|
||||
let attrs = this.attrs(node);
|
||||
attrs.push(['class', k_code_classes]);
|
||||
this.tag('code', attrs);
|
||||
this.out(node.literal);
|
||||
this.tag('/code');
|
||||
}
|
||||
|
||||
function attrs(node) {
|
||||
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
|
||||
if (node.type == 'block_quote') {
|
||||
result.push(['class', 'w3-theme-d1']);
|
||||
} else if (node.type == 'code_block') {
|
||||
result.push(['class', k_code_classes]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function markdown(md) {
|
||||
let reader = new commonmark.Parser();
|
||||
let writer = new commonmark.HtmlRenderer({safe: true});
|
||||
//writer.image = image;
|
||||
writer.code = code;
|
||||
writer.attrs = attrs;
|
||||
let parsed = reader.parse(md || '');
|
||||
let walker = parsed.walker();
|
||||
let event, node;
|
||||
while ((event = walker.next())) {
|
||||
node = event.node;
|
||||
if (event.entering) {
|
||||
if (node.type == 'link') {
|
||||
if (
|
||||
node.destination.startsWith('@') &&
|
||||
node.destination.endsWith('.ed25519')
|
||||
) {
|
||||
node.destination = '#' + encodeURIComponent(node.destination);
|
||||
} else if (
|
||||
node.destination.startsWith('%') &&
|
||||
node.destination.endsWith('.sha256')
|
||||
) {
|
||||
node.destination = '#' + encodeURIComponent(node.destination);
|
||||
} else if (
|
||||
node.destination.startsWith('&') &&
|
||||
node.destination.endsWith('.sha256')
|
||||
) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
} else if (node.type == 'image') {
|
||||
if (node.destination.startsWith('&')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let data = await query(`
|
||||
let whoami = await ssb.getActiveIdentity();
|
||||
let following = Object.keys(await ssb.following([whoami], 2));
|
||||
let data = await query(
|
||||
`
|
||||
SELECT
|
||||
messages.id,
|
||||
content ->> 'title' AS title,
|
||||
content ->> '$.image.link' AS image,
|
||||
content ->> 'description' AS description
|
||||
FROM messages
|
||||
content ->> 'description' AS description,
|
||||
content ->> 'authors' AS authors
|
||||
FROM messages, json_each(?) AS following
|
||||
ON messages.author = following.value
|
||||
WHERE
|
||||
content ->> 'type' = 'bookclub' AND
|
||||
title IS NOT NULL AND
|
||||
image IS NOT NULL AND
|
||||
description IS NOT NULL
|
||||
`);
|
||||
if (!data?.length) {
|
||||
await app.setDocument(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background-color: #fff">
|
||||
<p>No bookclub messages found.</p>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
return;
|
||||
`,
|
||||
[JSON.stringify(following)]
|
||||
);
|
||||
let books = Object.fromEntries(data.map((x) => [x.id, x]));
|
||||
for (let book of Object.values(books)) {
|
||||
try {
|
||||
book.authors = JSON.parse(book.authors);
|
||||
} catch {
|
||||
book.authors = [book.authors];
|
||||
}
|
||||
book.reviews = [];
|
||||
}
|
||||
await app.setDocument(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="w3.css">
|
||||
</head>
|
||||
<body class="w3-grid" style="background-color: #fff; gap:8px;grid-template-columns:repeat(auto-fit, minmax(4in,1fr))">
|
||||
${data
|
||||
.map(
|
||||
(x) => `
|
||||
<div class="w3-card-4">
|
||||
<header class="w3-container w3-center">
|
||||
<h1>${markdown(x.title)}</h1>
|
||||
</header>
|
||||
<div class="w3-container w3-center">
|
||||
<img src="/${x.image}/view" style="max-height: 2in; max-width: 2in">
|
||||
</div>
|
||||
<div class="w3-container">
|
||||
<p>${markdown(x.description)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('\n')}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
let reviews = await query(
|
||||
`
|
||||
SELECT author, about, json(json_group_object(key, value)) AS content
|
||||
FROM (
|
||||
SELECT
|
||||
messages.author,
|
||||
messages.content ->> '$.about' AS about,
|
||||
fields.key,
|
||||
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.about', fields.key ORDER BY messages.sequence DESC) AS rank,
|
||||
fields.value
|
||||
FROM messages, json_each(messages.content) AS fields, json_each(?) AS book, json_each(?) AS following
|
||||
ON messages.author = following.value
|
||||
WHERE
|
||||
messages.content ->> 'type' = 'about'
|
||||
AND messages.content ->> '$.about' = book.value
|
||||
AND NOT fields.key IN ('about', 'type')
|
||||
) WHERE rank = 1
|
||||
GROUP BY author, about
|
||||
`,
|
||||
[JSON.stringify(Object.keys(books)), JSON.stringify(following)]
|
||||
);
|
||||
for (let review of reviews) {
|
||||
review.content = JSON.parse(review.content);
|
||||
books[review.about].reviews.push(review);
|
||||
}
|
||||
await app.setDocument(
|
||||
utf8Decode(getFile('index.html')).replace('G_DATA', JSON.stringify(data))
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
main().catch(function (e) {
|
||||
throw new Error(e.message);
|
||||
});
|
||||
|
||||
51
apps/bookclub/bc-app.js
Normal file
51
apps/bookclub/bc-app.js
Normal 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);
|
||||
@@ -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
120
apps/bookclub/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/bookclub/lit-all.min.js.map
Normal file
1
apps/bookclub/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
119
apps/bookclub/markdown.js
Normal file
119
apps/bookclub/markdown.js
Normal 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));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "➡️",
|
||||
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
|
||||
"previous": "&WiC0IosFJw/FNuypqKc4LFZUZjCFlmbY7KEAabrXU9o=.sha256"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
`,
|
||||
[
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💡",
|
||||
"previous": "&FGkkfFLaEID3V4lUjPbgCOwgEvNXkcVkzs0zzwD/gQ8=.sha256"
|
||||
"previous": "&dez1mAjzd4X9Z6ss0cBJO8EJDP+g3GtyYDNiasrw2pM=.sha256"
|
||||
}
|
||||
|
||||
@@ -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> ->
|
||||
<b>Core Apps</b> -> <b>intro</b>.
|
||||
<b>Core Apps</b> -> <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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🦀",
|
||||
"previous": "&eqeAxU0q6n0RZDSd68j44hQ4UtssESqgohsCXN/otwY=.sha256"
|
||||
"previous": "&S0BuSm19vBzA6Gf97/rYcwndKyb55MxoITnz7xRjlzg=.sha256"
|
||||
}
|
||||
|
||||
@@ -105,10 +105,24 @@ class TfElement extends LitElement {
|
||||
await this.load_channels();
|
||||
}
|
||||
|
||||
async open_private_chat(event) {
|
||||
let update = {};
|
||||
update[event.detail.key] = false;
|
||||
this.private_closed = Object.assign(this.private_closed, update);
|
||||
await tfrpc.rpc.databaseSet(
|
||||
'private_closed',
|
||||
JSON.stringify(this.private_closed)
|
||||
);
|
||||
}
|
||||
|
||||
async close_private_chat(event) {
|
||||
let update = {};
|
||||
update[event.detail.key] = true;
|
||||
this.private_closed = Object.assign(update, this.private_closed);
|
||||
update[
|
||||
event.detail.key == '[]'
|
||||
? JSON.stringify([this.whoami])
|
||||
: event.detail.key
|
||||
] = true;
|
||||
this.private_closed = Object.assign(this.private_closed, update);
|
||||
await tfrpc.rpc.databaseSet(
|
||||
'private_closed',
|
||||
JSON.stringify(this.private_closed)
|
||||
@@ -167,16 +181,22 @@ class TfElement extends LitElement {
|
||||
return [];
|
||||
}
|
||||
let self = this;
|
||||
let self_key = JSON.stringify([this.whoami]);
|
||||
let opened = Object.entries(this.private_closed)
|
||||
.filter(([key, value]) => !value)
|
||||
.map(([key, value]) => [key, []]);
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.grouped_private_messages).filter(([key, value]) => {
|
||||
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
|
||||
let grouped_latest = Math.max(...value.map((x) => x.rowid));
|
||||
return (
|
||||
!self.private_closed[key] ||
|
||||
self.channels_unread[channel] === undefined ||
|
||||
grouped_latest > self.channels_unread[channel]
|
||||
);
|
||||
})
|
||||
[...Object.entries(this.grouped_private_messages), ...opened].filter(
|
||||
([key, value]) => {
|
||||
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
|
||||
let grouped_latest = Math.max(...value.map((x) => x.rowid));
|
||||
return (
|
||||
!self.private_closed[key] ||
|
||||
self.channels_unread[channel] === undefined ||
|
||||
grouped_latest > self.channels_unread[channel]
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,14 +211,18 @@ class TfElement extends LitElement {
|
||||
.map((x) => '🔐' + JSON.parse(x).join(',')),
|
||||
...this.channels.map((x) => '#' + x),
|
||||
];
|
||||
let index = channel_names.indexOf(this.hash.substring(1));
|
||||
let lookup = this.hash.substring(1);
|
||||
if (lookup == '🔐') {
|
||||
lookup = '🔐' + this.whoami;
|
||||
}
|
||||
let index = channel_names.indexOf(lookup);
|
||||
index = index != -1 ? index + delta : 0;
|
||||
tfrpc.rpc.setHash(
|
||||
'#' +
|
||||
encodeURIComponent(
|
||||
channel_names[(index + channel_names.length) % channel_names.length]
|
||||
)
|
||||
);
|
||||
let name =
|
||||
channel_names[(index + channel_names.length) % channel_names.length];
|
||||
if (name == '🔐' + this.whoami) {
|
||||
name = '🔐';
|
||||
}
|
||||
tfrpc.rpc.setHash('#' + encodeURIComponent(name));
|
||||
}
|
||||
|
||||
set_hash(hash) {
|
||||
@@ -212,7 +236,7 @@ class TfElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async fetch_about(following, users) {
|
||||
async fetch_about(following, users, transient) {
|
||||
this.loading_about++;
|
||||
let ids = Object.keys(following).sort();
|
||||
const k_cache_version = 3;
|
||||
@@ -260,7 +284,7 @@ class TfElement extends LitElement {
|
||||
fields.value
|
||||
FROM messages JOIN json_each(messages.content) AS fields
|
||||
WHERE
|
||||
messages.content ->> '$.type' = 'about' AND
|
||||
messages.content ->> 'type' = 'about' AND
|
||||
messages.content ->> '$.about' = messages.author AND
|
||||
NOT fields.key IN ('about', 'type')) all_abouts
|
||||
JOIN json_each(?) AS following ON all_abouts.author = following.value
|
||||
@@ -296,7 +320,7 @@ class TfElement extends LitElement {
|
||||
this.loading_about--;
|
||||
|
||||
let new_cache = JSON.stringify(cache);
|
||||
if (new_cache != original_cache) {
|
||||
if (!transient && new_cache != original_cache) {
|
||||
let start_time = new Date();
|
||||
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
|
||||
console.log('saving about took', (new Date() - start_time) / 1000);
|
||||
@@ -581,7 +605,7 @@ class TfElement extends LitElement {
|
||||
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||
FROM messages
|
||||
WHERE author = ? AND
|
||||
content ->> '$.type' = 'vote'
|
||||
content ->> 'type' = 'vote'
|
||||
ORDER BY timestamp DESC LIMIT 10
|
||||
`,
|
||||
[this.whoami]
|
||||
@@ -684,6 +708,7 @@ class TfElement extends LitElement {
|
||||
@refresh=${this.refresh}
|
||||
@toggle_stay_connected=${this.toggle_stay_connected}
|
||||
@loadmessages=${this.reset_progress}
|
||||
@openprivatechat=${this.open_private_chat}
|
||||
@closeprivatechat=${this.close_private_chat}
|
||||
.connections=${this.connections}
|
||||
.private_messages=${this.private_messages}
|
||||
@@ -777,6 +802,18 @@ class TfElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async request_user(event) {
|
||||
let users = {};
|
||||
users[event.detail.id] = {};
|
||||
users = await this.fetch_user_info(users);
|
||||
if (this.users[event.detail.id]?.seq !== users[event.detail.id]?.seq) {
|
||||
let self = this;
|
||||
this.fetch_about(users, users, true).then(function (result) {
|
||||
self.users = Object.assign({}, self.users, users);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
|
||||
@@ -889,6 +926,7 @@ class TfElement extends LitElement {
|
||||
<div
|
||||
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
|
||||
class="w3-theme-dark"
|
||||
@tf-request-user=${this.request_user}
|
||||
>
|
||||
${progress}
|
||||
<div style="flex: 0 0">${tabs}</div>
|
||||
|
||||
@@ -1166,7 +1166,17 @@ class TfMessageElement extends LitElement {
|
||||
);
|
||||
}
|
||||
} else if (typeof this.message.content == 'string') {
|
||||
return this.render_small_frame();
|
||||
if (this.message?.decrypted) {
|
||||
if (this.format == 'decrypted') {
|
||||
return this.render_small_frame(
|
||||
this.render_json(this.message.decrypted)
|
||||
);
|
||||
} else {
|
||||
return this.render_small_frame(this.message.decrypted.type);
|
||||
}
|
||||
} else {
|
||||
return this.render_small_frame();
|
||||
}
|
||||
} else {
|
||||
return this.render_small_frame(this.render_raw());
|
||||
}
|
||||
|
||||
@@ -241,6 +241,20 @@ class TfProfileElement extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
open_private_chat() {
|
||||
let hash = '#🔐' + (this.id != this.whoami ? this.id : '');
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('openprivatechat', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
key: JSON.stringify([this.id]),
|
||||
},
|
||||
})
|
||||
);
|
||||
tfrpc.rpc.setHash(hash);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.load();
|
||||
let self = this;
|
||||
@@ -360,9 +374,9 @@ class TfProfileElement extends LitElement {
|
||||
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
|
||||
<footer class="w3-container">
|
||||
<p>
|
||||
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
|
||||
<button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat">
|
||||
Open Private Chat
|
||||
</a>
|
||||
</button>
|
||||
${edit}
|
||||
${follow}
|
||||
${block}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "👋",
|
||||
"previous": "&m7sF3iaAEqf6PtaTBN+upiHNNBPjrq15kZUuxRWiAag=.sha256"
|
||||
"previous": "&IwbeqN5jcUWa8wbnWxduiwel9VldRO/dARDjljX33PM=.sha256"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">×</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"
|
||||
>×</span
|
||||
>
|
||||
<div
|
||||
style="color: ${this.status.color ??
|
||||
k_color_error}; display: block"
|
||||
>
|
||||
<b>ERROR:</b>
|
||||
<p id="error">${this.status.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: undefined}
|
||||
`;
|
||||
@@ -995,6 +1025,32 @@ function closeEditor() {
|
||||
document.getElementById('viewPane').style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload any static HTML content in the iframe.
|
||||
*/
|
||||
async function update_html() {
|
||||
try {
|
||||
let response = await fetch(window.location.href);
|
||||
let text = await response.text();
|
||||
let parser = new DOMParser();
|
||||
let new_doc = parser.parseFromString(text, 'text/html');
|
||||
let new_html = new_doc.getElementById('document').attributes.srcdoc.value;
|
||||
let iframe = document.getElementById('document');
|
||||
let sandbox = iframe.sandbox;
|
||||
|
||||
let iframe_parent = iframe.parentNode;
|
||||
iframe_parent.removeChild(iframe);
|
||||
|
||||
let new_iframe = document.createElement('iframe');
|
||||
new_iframe.sandbox = sandbox;
|
||||
new_iframe.id = 'document';
|
||||
new_iframe.srcdoc = new_html;
|
||||
iframe_parent.appendChild(new_iframe);
|
||||
} catch (e) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the app.
|
||||
* @param save_to An optional path to which to save the app.
|
||||
@@ -1090,7 +1146,7 @@ function save(save_to) {
|
||||
if (save_path != window.location.pathname) {
|
||||
alert('Saved to ' + save_path + '.');
|
||||
} else if (!g_files['app.js']) {
|
||||
window.location.reload();
|
||||
update_html();
|
||||
} else {
|
||||
reconnect(save_path);
|
||||
}
|
||||
@@ -1469,6 +1525,18 @@ function blur() {
|
||||
send({event: 'blur'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the app of visibility change. Seems to work when changing apps/tabs
|
||||
* where focus/blur doesn't on mobile.
|
||||
*/
|
||||
function visibilitychange() {
|
||||
if (!document.hidden) {
|
||||
if (g_socket && g_socket.readyState == g_socket.CLOSED) {
|
||||
connectSocket();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a message.
|
||||
* @param event The message.
|
||||
@@ -1857,6 +1925,7 @@ window.addEventListener('load', function () {
|
||||
window.addEventListener('beforeunload', function () {
|
||||
g_unloading = true;
|
||||
});
|
||||
document.addEventListener('visibilitychange', visibilitychange);
|
||||
document.getElementById('name').value = window.location.pathname;
|
||||
document
|
||||
.getElementById('closeEditor')
|
||||
|
||||
255
core/core.js
255
core/core.js
@@ -14,80 +14,6 @@ let g_handler_index = 0;
|
||||
/** Whether updating accounts information is currently scheduled. */
|
||||
let g_update_accounts_scheduled;
|
||||
|
||||
/**
|
||||
** App constructor.
|
||||
** @return An app instance.
|
||||
*/
|
||||
function App() {
|
||||
this._send_queue = [];
|
||||
this.calls = {};
|
||||
this._next_call_id = 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
** Create a function wrapper that when called invokes a function on the app
|
||||
** itself.
|
||||
** @param api The function and argument names.
|
||||
** @return A function.
|
||||
*/
|
||||
App.prototype.makeFunction = function (api) {
|
||||
let self = this;
|
||||
let result = function () {
|
||||
let id = self._next_call_id++;
|
||||
while (!id || self.calls[id]) {
|
||||
id = self._next_call_id++;
|
||||
}
|
||||
let promise = new Promise(function (resolve, reject) {
|
||||
self.calls[id] = {resolve: resolve, reject: reject};
|
||||
});
|
||||
let message = {
|
||||
action: 'tfrpc',
|
||||
method: api[0],
|
||||
params: [...arguments],
|
||||
id: id,
|
||||
};
|
||||
self.send(message);
|
||||
return promise;
|
||||
};
|
||||
Object.defineProperty(result, 'name', {value: api[0], writable: false});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
** Send a message to the app.
|
||||
** @param message The message to send.
|
||||
*/
|
||||
App.prototype.send = function (message) {
|
||||
if (this._send_queue) {
|
||||
if (this._on_output) {
|
||||
this._send_queue.forEach((x) => this._on_output(x));
|
||||
this._send_queue = null;
|
||||
} else if (message) {
|
||||
this._send_queue.push(message);
|
||||
}
|
||||
}
|
||||
if (message && this._on_output) {
|
||||
this._on_output(message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Print an error.
|
||||
* @param error The error.
|
||||
*/
|
||||
function printError(error) {
|
||||
if (error.stackTrace) {
|
||||
print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
|
||||
print(error.stackTrace);
|
||||
} else {
|
||||
for (let [k, v] of Object.entries(error)) {
|
||||
print(k, v);
|
||||
}
|
||||
print(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a handler.
|
||||
* @param handlers The handlers on which to invoke the callback.
|
||||
@@ -233,7 +159,62 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
process.url = options?.url;
|
||||
process.eventHandlers = {};
|
||||
if (!options?.script || options?.script === 'app.js') {
|
||||
process.app = new App();
|
||||
process._send_queue = [];
|
||||
process._calls = {};
|
||||
process._next_call_id = 1;
|
||||
|
||||
/**
|
||||
** Create a function wrapper that when called invokes a function on the app
|
||||
** itself.
|
||||
** @param api The function and argument names.
|
||||
** @return A function.
|
||||
*/
|
||||
process.makeFunction = function (api) {
|
||||
let result = function () {
|
||||
let id = process._next_call_id++;
|
||||
while (!id || process._calls[id]) {
|
||||
id = process._next_call_id++;
|
||||
}
|
||||
let promise = new Promise(function (resolve, reject) {
|
||||
process._calls[id] = {resolve: resolve, reject: reject};
|
||||
});
|
||||
let message = {
|
||||
action: 'tfrpc',
|
||||
method: api[0],
|
||||
params: [...arguments],
|
||||
id: id,
|
||||
};
|
||||
process.send(message);
|
||||
return promise;
|
||||
};
|
||||
Object.defineProperty(result, 'name', {
|
||||
value: api[0],
|
||||
writable: false,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
** Send a message to the app.
|
||||
** @param message The message to send.
|
||||
*/
|
||||
process.send = function (message) {
|
||||
if (process._send_queue) {
|
||||
if (process._on_output) {
|
||||
process._send_queue.forEach((x) => process._on_output(x));
|
||||
process._send_queue = null;
|
||||
} else if (message) {
|
||||
process._send_queue.push(message);
|
||||
}
|
||||
}
|
||||
if (message && process._on_output) {
|
||||
process._on_output(message);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
process.makeFunction = function (api) {
|
||||
return function () {};
|
||||
};
|
||||
}
|
||||
process.ready = new Promise(function (resolve, reject) {
|
||||
resolveReady = resolve;
|
||||
@@ -248,19 +229,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
core: {
|
||||
broadcast: broadcast.bind(process),
|
||||
user: getUser(process, process),
|
||||
allPermissionsGranted: async function () {
|
||||
let user = process?.credentials?.session?.name;
|
||||
let settings = await loadSettings();
|
||||
if (
|
||||
user &&
|
||||
options?.packageOwner &&
|
||||
options?.packageName &&
|
||||
settings.userPermissions &&
|
||||
settings.userPermissions[user]
|
||||
) {
|
||||
return settings.userPermissions[user];
|
||||
}
|
||||
},
|
||||
permissionTest: async function (permission, description) {
|
||||
let user = process?.credentials?.session?.name;
|
||||
let settings = await loadSettings();
|
||||
@@ -286,8 +254,8 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
} else {
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
}
|
||||
} else if (process.app) {
|
||||
return process.app
|
||||
} else {
|
||||
return process
|
||||
.makeFunction(['requestPermission'])(permission, description)
|
||||
.then(async function (value) {
|
||||
if (value == 'allow') {
|
||||
@@ -317,8 +285,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
}
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
});
|
||||
} else {
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -331,7 +297,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
);
|
||||
let json = JSON.stringify(identities);
|
||||
if (process._last_sent_identities !== json) {
|
||||
process.app.send(
|
||||
process.send(
|
||||
Object.assign(
|
||||
{
|
||||
action: 'identities',
|
||||
@@ -393,7 +359,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
imports.app = {};
|
||||
for (let i in options.api) {
|
||||
let api = options.api[i];
|
||||
imports.app[api[0]] = process.app.makeFunction(api);
|
||||
imports.app[api[0]] = process.makeFunction(api);
|
||||
}
|
||||
}
|
||||
for (let [name, f] of Object.entries(options?.imports || {})) {
|
||||
@@ -404,17 +370,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
imports.app.print(...args);
|
||||
}
|
||||
};
|
||||
process.task.onError = function (error) {
|
||||
try {
|
||||
if (process.app) {
|
||||
process.app.makeFunction(['error'])(error);
|
||||
} else {
|
||||
printError(error);
|
||||
}
|
||||
} catch (e) {
|
||||
printError(error);
|
||||
}
|
||||
};
|
||||
process.task.onError = process.makeFunction(['error']);
|
||||
imports.ssb = Object.fromEntries(
|
||||
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
|
||||
);
|
||||
@@ -459,33 +415,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
});
|
||||
}
|
||||
};
|
||||
imports.ssb.appendMessageWithIdentity = function (id, message) {
|
||||
if (
|
||||
process.credentials &&
|
||||
process.credentials.session &&
|
||||
process.credentials.session.name
|
||||
) {
|
||||
let action;
|
||||
try {
|
||||
if (message?.type === 'vote' && message?.vote?.expression) {
|
||||
action = `React with ${message?.vote?.expression}.`;
|
||||
} else if (typeof message === 'string') {
|
||||
action = `Post a private message.`;
|
||||
} else {
|
||||
action = `Publish ${'aeiou'.indexOf(message?.type?.toLowerCase()?.substring(0, 1)) != -1 ? 'an' : 'a'} "${message?.type}" message.`;
|
||||
}
|
||||
} catch {}
|
||||
return Promise.resolve(
|
||||
imports.core.permissionTest('ssb_append', action)
|
||||
).then(function () {
|
||||
return ssb.appendMessageWithIdentity(
|
||||
process.credentials.session.name,
|
||||
id,
|
||||
message
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
if (
|
||||
process.credentials &&
|
||||
process.credentials.session &&
|
||||
@@ -535,7 +464,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
};
|
||||
}
|
||||
process.sendPermissions = async function sendPermissions() {
|
||||
process.app.send({
|
||||
process.send({
|
||||
action: 'permissions',
|
||||
permissions: await imports.core.permissionsGranted(),
|
||||
});
|
||||
@@ -566,31 +495,27 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
let source = await ssb.blobGet(blobId);
|
||||
let appSourceName = blobId;
|
||||
let appSource = utf8Decode(source);
|
||||
try {
|
||||
let appObject = JSON.parse(appSource);
|
||||
if (appObject.type == 'tildefriends-app') {
|
||||
appSourceName = options?.script ?? 'app.js';
|
||||
let id = appObject.files[appSourceName];
|
||||
let blob = await ssb.blobGet(id);
|
||||
appSource = utf8Decode(blob);
|
||||
await process.task.loadFile([
|
||||
'/tfrpc.js',
|
||||
await File.readFile('core/tfrpc.js'),
|
||||
]);
|
||||
await Promise.all(
|
||||
Object.keys(appObject.files).map(async function (f) {
|
||||
await process.task.loadFile([
|
||||
f,
|
||||
await ssb.blobGet(appObject.files[f]),
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
printError(e);
|
||||
let appObject = JSON.parse(appSource);
|
||||
if (appObject.type == 'tildefriends-app') {
|
||||
appSourceName = options?.script ?? 'app.js';
|
||||
let id = appObject.files[appSourceName];
|
||||
let blob = await ssb.blobGet(id);
|
||||
appSource = utf8Decode(blob);
|
||||
await process.task.loadFile([
|
||||
'/tfrpc.js',
|
||||
await File.readFile('core/tfrpc.js'),
|
||||
]);
|
||||
await Promise.all(
|
||||
Object.keys(appObject.files).map(async function (f) {
|
||||
await process.task.loadFile([
|
||||
f,
|
||||
await ssb.blobGet(appObject.files[f]),
|
||||
]);
|
||||
})
|
||||
);
|
||||
}
|
||||
if (process.app) {
|
||||
process.app.send({action: 'ready', version: version()});
|
||||
if (process.send) {
|
||||
process.send({action: 'ready', version: version()});
|
||||
await process.sendPermissions();
|
||||
}
|
||||
await process.task.execute({name: appSourceName, source: appSource});
|
||||
@@ -600,12 +525,12 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
|
||||
sendStats();
|
||||
}
|
||||
} catch (error) {
|
||||
if (process?.app && process?.task?.onError) {
|
||||
if (process?.task?.onError) {
|
||||
process.task.onError(error);
|
||||
} else {
|
||||
printError(error);
|
||||
}
|
||||
rejectReady(error);
|
||||
if (rejectReady) {
|
||||
rejectReady(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return process;
|
||||
@@ -673,13 +598,11 @@ async function loadSettings() {
|
||||
* Send periodic stats to all clients.
|
||||
*/
|
||||
function sendStats() {
|
||||
let apps = Object.values(gProcesses)
|
||||
.filter((process) => process.app)
|
||||
.map((process) => process.app);
|
||||
let apps = Object.values(gProcesses).filter((process) => process.send);
|
||||
if (apps.length) {
|
||||
let stats = getStats();
|
||||
for (let app of apps) {
|
||||
app.send({action: 'stats', stats: stats});
|
||||
for (let process of apps) {
|
||||
process.send({action: 'stats', stats: stats});
|
||||
}
|
||||
setTimeout(sendStats, 1000);
|
||||
} else {
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
class="vbox"
|
||||
style="flex: 0 1 100%; display: none; overflow: auto"
|
||||
>
|
||||
<div class="navigation w3-bar" style="display: flex">
|
||||
<div class="w3-bar w3-blue">
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="closeEditor"
|
||||
@@ -77,16 +77,6 @@
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="save"
|
||||
name="save"
|
||||
accesskey="s"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Save the app under the given path"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="icon"
|
||||
@@ -137,13 +127,6 @@
|
||||
>
|
||||
✨
|
||||
</button>
|
||||
<input
|
||||
class="w3-bar-item w3-input w3-border w3-blue"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
style="flex: 1 1; min-width: 1em"
|
||||
/>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue"
|
||||
id="delete"
|
||||
@@ -163,6 +146,23 @@
|
||||
>
|
||||
Trace
|
||||
</button>
|
||||
<button
|
||||
class="w3-bar-item w3-button w3-blue w3-right"
|
||||
id="save"
|
||||
name="save"
|
||||
accesskey="s"
|
||||
onmouseover="set_access_key_title(event)"
|
||||
data-tip="Save the app under the given path"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<input
|
||||
class="w3-bar-item w3-input w3-cobalt w3-right"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
style="min-width: 1em"
|
||||
/>
|
||||
</div>
|
||||
<div class="hbox" style="flex: 1 1; overflow: auto">
|
||||
<div style="overflow: auto">
|
||||
@@ -178,7 +178,6 @@
|
||||
<iframe
|
||||
id="document"
|
||||
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
|
||||
style="width: 100%; height: 100%; border: 0"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -154,3 +154,9 @@ body {
|
||||
margin: 0 auto;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
#document {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
2
deps/codemirror/cm6.js
vendored
2
deps/codemirror/cm6.js
vendored
File diff suppressed because one or more lines are too long
222
deps/codemirror_src/package-lock.json
generated
vendored
222
deps/codemirror_src/package-lock.json
generated
vendored
@@ -129,14 +129,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
|
||||
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
|
||||
"version": "6.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
@@ -165,9 +165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
|
||||
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
@@ -186,9 +186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.39.4",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
|
||||
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
|
||||
"version": "6.39.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.6.tgz",
|
||||
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
@@ -248,9 +248,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz",
|
||||
"integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
|
||||
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
@@ -274,9 +274,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
|
||||
"integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
@@ -316,12 +316,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.1.tgz",
|
||||
"integrity": "sha512-72ah+Sml7lD8Wn7lnz9vwYmZBo9aQT+I2gjK/0epI+gjdwUbWw3MJ/ZBGEqG1UfrIauRqH37/c5mVHXeCTGXtA==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.2.tgz",
|
||||
"integrity": "sha512-iNSdKrIK0FfOjVPVpV0fu7OykdncYpEzf4vkG9szFf60ql/ObZShoVbM9u1tgkogDOmubms1CyoNS2/unOXWNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
@@ -412,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
|
||||
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -425,9 +425,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -438,9 +438,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -451,9 +451,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
|
||||
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -464,9 +464,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -477,9 +477,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
|
||||
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -490,9 +490,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
|
||||
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -503,9 +503,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
|
||||
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -516,9 +516,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -529,9 +529,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -542,9 +542,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -555,9 +555,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -568,9 +568,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -581,9 +581,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -594,9 +594,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -607,9 +607,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -620,9 +620,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
|
||||
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -633,9 +633,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
|
||||
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -646,9 +646,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -659,9 +659,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -672,9 +672,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -685,9 +685,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -877,9 +877,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.53.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
|
||||
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -892,28 +892,28 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.53.5",
|
||||
"@rollup/rollup-android-arm64": "4.53.5",
|
||||
"@rollup/rollup-darwin-arm64": "4.53.5",
|
||||
"@rollup/rollup-darwin-x64": "4.53.5",
|
||||
"@rollup/rollup-freebsd-arm64": "4.53.5",
|
||||
"@rollup/rollup-freebsd-x64": "4.53.5",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.53.5",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.53.5",
|
||||
"@rollup/rollup-linux-x64-musl": "4.53.5",
|
||||
"@rollup/rollup-openharmony-arm64": "4.53.5",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.53.5",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.53.5",
|
||||
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
||||
"@rollup/rollup-android-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-x64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
8
flake.lock
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
|
||||
6
metadata/en-US/changelogs/49.txt
Normal file
6
metadata/en-US/changelogs/49.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
* Crash fixes.
|
||||
* Fix channels with hyphens and various other characters not working correctly.
|
||||
* Navigation bar and search UI improvements.
|
||||
* Faster loads, though the first launch may be particularly slow as indexes are rebuilt.
|
||||
* Fixed various broken links.
|
||||
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1.
|
||||
219
src/api.js.c
219
src/api.js.c
@@ -24,6 +24,8 @@
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
|
||||
static JSClassID s_permission_test_class_id;
|
||||
|
||||
typedef struct _app_path_pair_t
|
||||
{
|
||||
const char* app;
|
||||
@@ -417,6 +419,52 @@ static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst
|
||||
return result;
|
||||
}
|
||||
|
||||
static void _tf_api_core_all_permissions_granted_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||
{
|
||||
permissions_granted_t* work = user_data;
|
||||
JSContext* context = work->context;
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (work->settings)
|
||||
{
|
||||
JSValue json = JS_ParseJSON(context, work->settings, strlen(work->settings), NULL);
|
||||
if (JS_IsObject(json) && work->user)
|
||||
{
|
||||
JSValue user_permissions = JS_GetPropertyStr(context, json, "userPermissions");
|
||||
if (JS_IsObject(user_permissions))
|
||||
{
|
||||
result = JS_GetPropertyStr(context, user_permissions, work->user);
|
||||
}
|
||||
JS_FreeValue(context, user_permissions);
|
||||
}
|
||||
JS_FreeValue(context, json);
|
||||
tf_free((void*)work->settings);
|
||||
}
|
||||
|
||||
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
|
||||
tf_util_report_error(context, error);
|
||||
JS_FreeValue(context, error);
|
||||
JS_FreeValue(context, result);
|
||||
JS_FreeValue(context, work->promise[0]);
|
||||
JS_FreeValue(context, work->promise[1]);
|
||||
JS_FreeCString(context, work->user);
|
||||
tf_free(work);
|
||||
}
|
||||
|
||||
static JSValue _tf_api_core_allPermissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
|
||||
{
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
JSValue process = data[0];
|
||||
permissions_granted_t* work = tf_malloc(sizeof(permissions_granted_t));
|
||||
*work = (permissions_granted_t) {
|
||||
.context = context,
|
||||
.user = _tf_ssb_get_process_credentials_session_name(context, process),
|
||||
};
|
||||
JSValue result = JS_NewPromiseCapability(context, work->promise);
|
||||
tf_ssb_run_work(ssb, _tf_api_core_permissions_granted_work, _tf_api_core_all_permissions_granted_after_work, work);
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _active_identity_work_t
|
||||
{
|
||||
JSContext* context;
|
||||
@@ -838,38 +886,59 @@ typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSVal
|
||||
|
||||
typedef struct _permission_test_t
|
||||
{
|
||||
JSContext* context;
|
||||
bool completed;
|
||||
permission_test_callback_t* callback;
|
||||
void* user_data;
|
||||
} permission_test_t;
|
||||
|
||||
static JSValue _tf_ssb_permission_test_resolve(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
|
||||
{
|
||||
JSClassID class_id = 0;
|
||||
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
|
||||
JS_FreeValue(context, data[0]);
|
||||
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
|
||||
work->completed = true;
|
||||
work->callback(context, true, argv[0], work->user_data);
|
||||
tf_free(work);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_permission_test_reject(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
|
||||
{
|
||||
JSClassID class_id = 0;
|
||||
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
|
||||
JS_FreeValue(context, data[0]);
|
||||
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
|
||||
work->completed = true;
|
||||
work->callback(context, false, argv[0], work->user_data);
|
||||
tf_free(work);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
|
||||
{
|
||||
permission_test_t* work = JS_GetOpaque(value, s_permission_test_class_id);
|
||||
if (!work->completed)
|
||||
{
|
||||
JSValue arg = JS_ThrowInternalError(work->context, "Permission test incomplete.");
|
||||
work->callback(work->context, false, arg, work->user_data);
|
||||
JS_FreeValue(work->context, arg);
|
||||
}
|
||||
tf_free(work);
|
||||
}
|
||||
|
||||
static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data)
|
||||
{
|
||||
if (!s_permission_test_class_id)
|
||||
{
|
||||
JSClassDef def = {
|
||||
.class_name = "permission_test",
|
||||
.finalizer = _tf_ssb_permission_test_finalizer,
|
||||
};
|
||||
JS_NewClassID(&s_permission_test_class_id);
|
||||
JS_NewClass(JS_GetRuntime(context), s_permission_test_class_id, &def);
|
||||
}
|
||||
|
||||
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
|
||||
*payload = (permission_test_t) {
|
||||
.context = context,
|
||||
.callback = callback,
|
||||
.user_data = user_data,
|
||||
};
|
||||
JSValue opaque = JS_NewObject(context);
|
||||
JSValue opaque = JS_NewObjectClass(context, s_permission_test_class_id);
|
||||
JS_SetOpaque(opaque, payload);
|
||||
JSValue imports = JS_GetPropertyStr(context, process, "imports");
|
||||
JSValue core = JS_GetPropertyStr(context, imports, "core");
|
||||
@@ -888,10 +957,11 @@ static void _tf_ssb_permission_test(JSContext* context, JSValue process, const c
|
||||
JS_FreeValue(context, result);
|
||||
result = JS_Call(context, catch, promise, 1, &reject);
|
||||
tf_util_report_error(context, result);
|
||||
JS_FreeValue(context, result);
|
||||
JS_FreeValue(context, opaque);
|
||||
JS_FreeValue(context, promise);
|
||||
JS_FreeValue(context, resolve);
|
||||
JS_FreeValue(context, reject);
|
||||
JS_FreeValue(context, result);
|
||||
JS_FreeValue(context, then);
|
||||
JS_FreeValue(context, catch);
|
||||
for (int i = 0; i < tf_countof(args); i++)
|
||||
@@ -1531,6 +1601,133 @@ static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _append_message_t
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||
bool got_private_key;
|
||||
char previous_id[512];
|
||||
int32_t previous_sequence;
|
||||
JSContext* context;
|
||||
JSValue promise[2];
|
||||
JSValue message;
|
||||
char user[];
|
||||
} append_message_t;
|
||||
|
||||
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
|
||||
{
|
||||
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
|
||||
tf_util_report_error(async->context, error);
|
||||
JS_FreeValue(async->context, error);
|
||||
JS_FreeValue(async->context, async->message);
|
||||
JS_FreeValue(async->context, async->promise[0]);
|
||||
JS_FreeValue(async->context, async->promise[1]);
|
||||
tf_free(async);
|
||||
}
|
||||
|
||||
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
|
||||
{
|
||||
append_message_t* async = user_data;
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (verified)
|
||||
{
|
||||
result = is_new ? JS_TRUE : JS_FALSE;
|
||||
}
|
||||
_tf_ssb_appendMessage_finish(async, verified, result);
|
||||
}
|
||||
|
||||
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
|
||||
{
|
||||
append_message_t* work = user_data;
|
||||
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
|
||||
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
|
||||
{
|
||||
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
|
||||
}
|
||||
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
|
||||
}
|
||||
|
||||
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||
{
|
||||
append_message_t* work = user_data;
|
||||
if (work->got_private_key)
|
||||
{
|
||||
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
|
||||
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
|
||||
JS_FreeValue(work->context, signed_message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
|
||||
}
|
||||
}
|
||||
|
||||
static void _tf_ssb_append_message_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
|
||||
{
|
||||
tf_task_t* task = tf_task_get(context);
|
||||
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
||||
append_message_t* work = user_data;
|
||||
if (granted)
|
||||
{
|
||||
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tf_ssb_appendMessage_finish(work, false, value);
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
|
||||
{
|
||||
JSValue process = data[0];
|
||||
|
||||
char description[1024] = "Publish a new message.";
|
||||
const char* type = tf_util_get_property_as_string(context, argv[1], "type");
|
||||
if (type)
|
||||
{
|
||||
if (strcmp(type, "vote") == 0)
|
||||
{
|
||||
JSValue vote = JS_GetPropertyStr(context, argv[1], "vote");
|
||||
const char* expression = tf_util_get_property_as_string(context, vote, "expression");
|
||||
snprintf(description, sizeof(description), "React with %s.", expression);
|
||||
JS_FreeCString(context, expression);
|
||||
JS_FreeValue(context, vote);
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(description, sizeof(description), "Publish a new %s message.", type);
|
||||
}
|
||||
}
|
||||
else if (JS_IsString(argv[1]))
|
||||
{
|
||||
tf_string_set(description, sizeof(description), "Publish a new private message.");
|
||||
}
|
||||
JS_FreeCString(context, type);
|
||||
|
||||
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
|
||||
if (!user)
|
||||
{
|
||||
return JS_ThrowInternalError(context, "Invalid user.");
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t user_length = strlen(user);
|
||||
const char* id = JS_ToCString(context, argv[0]);
|
||||
|
||||
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
|
||||
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[1]) };
|
||||
memcpy(work->user, user, user_length + 1);
|
||||
tf_string_set(work->id, sizeof(work->id), id);
|
||||
|
||||
JS_FreeCString(context, user);
|
||||
JS_FreeCString(context, id);
|
||||
|
||||
JSValue result = JS_NewPromiseCapability(context, work->promise);
|
||||
_tf_ssb_permission_test(context, process, "ssb_append", description, _tf_ssb_append_message_permission_callback, work);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
JSValue imports = argv[0];
|
||||
@@ -1544,6 +1741,7 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
|
||||
|
||||
JS_SetPropertyStr(context, core, "permissionsForUser", JS_NewCFunctionData(context, _tf_api_core_permissionsForUser, 1, 0, 1, &process));
|
||||
JS_SetPropertyStr(context, core, "permissionsGranted", JS_NewCFunctionData(context, _tf_api_core_permissionsGranted, 0, 0, 1, &process));
|
||||
JS_SetPropertyStr(context, core, "allPermissionsGranted", JS_NewCFunctionData(context, _tf_api_core_allPermissionsGranted, 0, 0, 1, &process));
|
||||
|
||||
JSValue app = JS_NewObject(context);
|
||||
JS_SetPropertyStr(context, app, "owner", JS_GetPropertyStr(context, process, "packageOwner"));
|
||||
@@ -1559,6 +1757,7 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
|
||||
JS_SetPropertyStr(context, ssb, "getOwnerIdentities", JS_NewCFunctionData(context, _tf_ssb_getOwnerIdentities, 0, 0, 1, &process));
|
||||
JS_SetPropertyStr(context, ssb, "privateMessageEncrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_encrypt, 3, 0, 1, &process));
|
||||
JS_SetPropertyStr(context, ssb, "privateMessageDecrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_decrypt, 2, 0, 1, &process));
|
||||
JS_SetPropertyStr(context, ssb, "appendMessageWithIdentity", JS_NewCFunctionData(context, _tf_ssb_appendMessageWithIdentity, 2, 0, 1, &process));
|
||||
JS_FreeValue(context, ssb);
|
||||
|
||||
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
16
src/ssb.c
16
src/ssb.c
@@ -2838,6 +2838,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
|
||||
ssb->broadcasts_count--;
|
||||
tf_free(broadcast);
|
||||
}
|
||||
uv_mutex_lock(&ssb->db_readers_lock);
|
||||
for (int i = 0; i < ssb->db_readers_count; i++)
|
||||
{
|
||||
int r = sqlite3_close(ssb->db_readers[i]);
|
||||
@@ -2846,6 +2847,14 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
|
||||
tf_printf("sqlite3_close: %s\n", sqlite3_errstr(r));
|
||||
}
|
||||
}
|
||||
if (ssb->db_readers)
|
||||
{
|
||||
tf_free(ssb->db_readers);
|
||||
ssb->db_readers = NULL;
|
||||
}
|
||||
ssb->db_readers_count = 0;
|
||||
uv_mutex_unlock(&ssb->db_readers_lock);
|
||||
uv_mutex_lock(&ssb->db_writer_lock);
|
||||
if (ssb->db_writer)
|
||||
{
|
||||
int r = sqlite3_close(ssb->db_writer);
|
||||
@@ -2855,12 +2864,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
|
||||
}
|
||||
ssb->db_writer = NULL;
|
||||
}
|
||||
ssb->db_readers_count = 0;
|
||||
if (ssb->db_readers)
|
||||
{
|
||||
tf_free(ssb->db_readers);
|
||||
ssb->db_readers = NULL;
|
||||
}
|
||||
uv_mutex_unlock(&ssb->db_writer_lock);
|
||||
if (ssb->db_path)
|
||||
{
|
||||
tf_free((void*)ssb->db_path);
|
||||
|
||||
87
src/ssb.db.c
87
src/ssb.db.c
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "log.h"
|
||||
#include "mem.h"
|
||||
#include "ssb.ebt.h"
|
||||
#include "ssb.h"
|
||||
#include "trace.h"
|
||||
#include "util.js.h"
|
||||
@@ -440,11 +441,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
|
||||
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_id_idx ON blob_wants_cache (id)");
|
||||
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_timestamp_id_idx ON blob_wants_cache (timestamp, id)");
|
||||
|
||||
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_blob_wants_cache");
|
||||
_tf_ssb_db_exec(db,
|
||||
"CREATE TRIGGER IF NOT EXISTS messages_ai_blob_wants_cache AFTER INSERT ON messages_refs BEGIN "
|
||||
"INSERT INTO blob_wants_cache (source, id, timestamp) "
|
||||
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
|
||||
"JOIN blobs ON new.ref = blobs.id "
|
||||
"LEFT OUTER JOIN blobs ON new.ref = blobs.id "
|
||||
"WHERE messages.id = new.message AND "
|
||||
"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' AND "
|
||||
"blobs.content IS NULL "
|
||||
@@ -455,7 +457,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
|
||||
"INSERT INTO blob_wants_cache (source, id, timestamp) "
|
||||
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
|
||||
"JOIN blob_wants_cache bwc ON bwc.source = messages.id AND bwc.id = new.blob "
|
||||
"JOIN blobs ON bwc.id = blobs.id "
|
||||
"LEFT OUTER JOIN blobs ON bwc.id = blobs.id "
|
||||
"WHERE blobs.content IS NULL "
|
||||
"ON CONFLICT (source, id) DO NOTHING; END");
|
||||
_tf_ssb_db_exec(db,
|
||||
@@ -534,46 +536,36 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
|
||||
return exists;
|
||||
}
|
||||
|
||||
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp, const char* content,
|
||||
size_t content_len, const char* signature, int flags)
|
||||
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, sqlite3_stmt* statement, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp,
|
||||
const char* content, size_t content_len, const char* signature, int flags)
|
||||
{
|
||||
int64_t last_row_id = -1;
|
||||
bool id_mismatch = false;
|
||||
|
||||
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
|
||||
{
|
||||
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
|
||||
"?, ?, ?) ON CONFLICT DO NOTHING";
|
||||
sqlite3_stmt* statement;
|
||||
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
|
||||
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
|
||||
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
|
||||
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
|
||||
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
|
||||
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
|
||||
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
|
||||
{
|
||||
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
|
||||
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
|
||||
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
|
||||
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
|
||||
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
|
||||
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
|
||||
int r = sqlite3_step(statement);
|
||||
if (r != SQLITE_DONE)
|
||||
{
|
||||
int r = sqlite3_step(statement);
|
||||
if (r != SQLITE_DONE)
|
||||
{
|
||||
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
|
||||
{
|
||||
last_row_id = sqlite3_last_insert_rowid(db);
|
||||
}
|
||||
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
else
|
||||
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
|
||||
{
|
||||
tf_printf("bind failed\n");
|
||||
last_row_id = sqlite3_last_insert_rowid(db);
|
||||
}
|
||||
sqlite3_finalize(statement);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
|
||||
tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
|
||||
}
|
||||
sqlite3_reset(statement);
|
||||
}
|
||||
else if (id_mismatch)
|
||||
{
|
||||
@@ -665,16 +657,27 @@ static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
|
||||
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
|
||||
bool in_transaction = _tf_ssb_db_try_exec(db, "BEGIN TRANSACTION") == SQLITE_OK;
|
||||
|
||||
while (store)
|
||||
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
|
||||
"?, ?, ?) ON CONFLICT DO NOTHING";
|
||||
sqlite3_stmt* insert_statement = NULL;
|
||||
if (sqlite3_prepare_v2(db, query, -1, &insert_statement, NULL) == SQLITE_OK)
|
||||
{
|
||||
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp,
|
||||
store->content, store->length, store->signature, store->flags);
|
||||
if (last_row_id != -1)
|
||||
while (store)
|
||||
{
|
||||
store->out_stored = true;
|
||||
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
|
||||
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, insert_statement, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence,
|
||||
store->timestamp, store->content, store->length, store->signature, store->flags);
|
||||
if (last_row_id != -1)
|
||||
{
|
||||
store->out_stored = true;
|
||||
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
|
||||
}
|
||||
store = store->next;
|
||||
}
|
||||
store = store->next;
|
||||
sqlite3_finalize(insert_statement);
|
||||
}
|
||||
else
|
||||
{
|
||||
tf_printf("prepare failed: %s.\n", sqlite3_errmsg(db));
|
||||
}
|
||||
|
||||
if (in_transaction)
|
||||
@@ -760,6 +763,22 @@ static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void*
|
||||
store = store->next;
|
||||
}
|
||||
|
||||
tf_ssb_connection_t* connections[256];
|
||||
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
|
||||
store = user_data;
|
||||
while (store)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tf_ssb_connection_t* connection = connections[i];
|
||||
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
|
||||
{
|
||||
tf_ssb_ebt_set_messages_received(tf_ssb_connection_get_ebt(connections[i]), store->author, store->sequence);
|
||||
}
|
||||
}
|
||||
store = store->next;
|
||||
}
|
||||
|
||||
if (last_stored)
|
||||
{
|
||||
tf_trace_begin(trace, "notify_message_added");
|
||||
|
||||
122
src/ssb.js.c
122
src/ssb.js.c
@@ -18,8 +18,6 @@ static const int k_sql_async_timeout_ms = 60 * 1000;
|
||||
|
||||
static JSClassID _tf_ssb_classId;
|
||||
|
||||
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
|
||||
|
||||
typedef struct _create_identity_t
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
@@ -390,92 +388,6 @@ static JSValue _tf_ssb_getIdentityInfo(JSContext* context, JSValueConst this_val
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _append_message_t
|
||||
{
|
||||
char id[k_id_base64_len];
|
||||
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
|
||||
bool got_private_key;
|
||||
char previous_id[512];
|
||||
int32_t previous_sequence;
|
||||
JSContext* context;
|
||||
JSValue promise[2];
|
||||
JSValue message;
|
||||
char user[];
|
||||
} append_message_t;
|
||||
|
||||
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
|
||||
{
|
||||
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
|
||||
tf_util_report_error(async->context, error);
|
||||
JS_FreeValue(async->context, error);
|
||||
JS_FreeValue(async->context, async->message);
|
||||
JS_FreeValue(async->context, async->promise[0]);
|
||||
JS_FreeValue(async->context, async->promise[1]);
|
||||
tf_free(async);
|
||||
}
|
||||
|
||||
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
|
||||
{
|
||||
append_message_t* async = user_data;
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (verified)
|
||||
{
|
||||
result = is_new ? JS_TRUE : JS_FALSE;
|
||||
}
|
||||
_tf_ssb_appendMessage_finish(async, verified, result);
|
||||
}
|
||||
|
||||
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
|
||||
{
|
||||
append_message_t* work = user_data;
|
||||
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
|
||||
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
|
||||
{
|
||||
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
|
||||
}
|
||||
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
|
||||
}
|
||||
|
||||
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
||||
{
|
||||
append_message_t* work = user_data;
|
||||
if (work->got_private_key)
|
||||
{
|
||||
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
|
||||
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
|
||||
JS_FreeValue(work->context, signed_message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
|
||||
{
|
||||
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
|
||||
if (!ssb)
|
||||
{
|
||||
return JS_ThrowInternalError(context, "No SSB instance.");
|
||||
}
|
||||
|
||||
size_t user_length = 0;
|
||||
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
|
||||
const char* id = JS_ToCString(context, argv[1]);
|
||||
|
||||
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
|
||||
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[2]) };
|
||||
memcpy(work->user, user, user_length + 1);
|
||||
tf_string_set(work->id, sizeof(work->id), id);
|
||||
|
||||
JS_FreeCString(context, id);
|
||||
JS_FreeCString(context, user);
|
||||
|
||||
JSValue result = JS_NewPromiseCapability(context, work->promise);
|
||||
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
|
||||
return result;
|
||||
}
|
||||
|
||||
typedef struct _blob_get_t
|
||||
{
|
||||
JSContext* context;
|
||||
@@ -923,13 +835,14 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
|
||||
sql_work_t* sql_work = user_data;
|
||||
JSContext* context = tf_ssb_get_context(ssb);
|
||||
uint8_t* p = sql_work->rows;
|
||||
while (p < sql_work->rows + sql_work->rows_count)
|
||||
JSValue result = JS_UNDEFINED;
|
||||
while (p < sql_work->rows + sql_work->rows_count && JS_IsUndefined(result))
|
||||
{
|
||||
if (*p++ == 'r')
|
||||
{
|
||||
JSValue row = JS_NewObject(context);
|
||||
|
||||
while (*p == 'c')
|
||||
while (*p == 'c' && JS_IsUndefined(result))
|
||||
{
|
||||
p++;
|
||||
const char* column_name = (const char*)p;
|
||||
@@ -970,14 +883,13 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
|
||||
}
|
||||
}
|
||||
|
||||
JSValue response = JS_Call(context, sql_work->callback, JS_UNDEFINED, 1, &row);
|
||||
bool is_error = tf_util_report_error(context, response);
|
||||
JS_FreeValue(context, response);
|
||||
JS_FreeValue(context, row);
|
||||
if (is_error)
|
||||
result = JS_Call(context, sql_work->callback, JS_UNDEFINED, 1, &row);
|
||||
if (!JS_IsException(result))
|
||||
{
|
||||
break;
|
||||
JS_FreeValue(context, result);
|
||||
result = JS_UNDEFINED;
|
||||
}
|
||||
JS_FreeValue(context, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -985,8 +897,20 @@ static void _tf_ssb_sqlAsync_after_work(tf_ssb_t* ssb, int status, void* user_da
|
||||
}
|
||||
}
|
||||
|
||||
JSValue result = JS_UNDEFINED;
|
||||
if (sql_work->result == SQLITE_OK || sql_work->result == SQLITE_DONE)
|
||||
if (!JS_IsUndefined(result))
|
||||
{
|
||||
bool is_exception = JS_IsException(result);
|
||||
if (is_exception)
|
||||
{
|
||||
JSValue exception = JS_GetException(context);
|
||||
JS_FreeValue(context, result);
|
||||
result = exception;
|
||||
}
|
||||
JSValue promise_result = JS_Call(context, sql_work->promise[is_exception ? 1 : 0], JS_UNDEFINED, 1, &result);
|
||||
tf_util_report_error(context, promise_result);
|
||||
JS_FreeValue(context, promise_result);
|
||||
}
|
||||
else if (sql_work->result == SQLITE_OK || sql_work->result == SQLITE_DONE)
|
||||
{
|
||||
result = JS_Call(context, sql_work->promise[0], JS_UNDEFINED, 0, NULL);
|
||||
tf_util_report_error(context, result);
|
||||
@@ -1783,8 +1707,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
|
||||
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
|
||||
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
|
||||
JS_SetPropertyStr(context, object, "setUserPermission", JS_NewCFunction(context, _tf_ssb_set_user_permission, "setUserPermission", 5));
|
||||
/* Write. */
|
||||
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
|
||||
|
||||
/* Does not require an identity. */
|
||||
JS_SetPropertyStr(context, object, "getServerIdentity", JS_NewCFunction(context, _tf_ssb_getServerIdentity, "getServerIdentity", 0));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',))
|
||||
|
||||
Reference in New Issue
Block a user