56 Commits

Author SHA1 Message Date
c0ed9fda01 test: Post and view a private message in the test.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 11m20s
2025-12-18 12:39:48 -05:00
95d263e139 core: Merge App into the process object.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 20:44:27 -05:00
782013f3a3 docs: Prepare some release notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 3m4s
Build Tilde Friends / Build-All (push) Successful in 9m58s
2025-12-17 20:16:57 -05:00
a5ed64f866 ssb: Fixing private messaging yourself, and delete a failing not-quite-relevant private message test.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m24s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 20:05:14 -05:00
88e3494dcf ssb: Finish moving private message encrypt/decrypt to C. 2025-12-17 19:46:26 -05:00
abe16dcf66 linux: Request no new privileges: https://docs.kernel.org/userspace-api/no_new_privs.html.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 19:05:43 -05:00
03a32ca371 ssb: Stop following yourself.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m24s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-17 18:58:09 -05:00
09a4fae432 ssb: Consolidate/fix following and blocking messaging. 2025-12-17 18:55:07 -05:00
5c173b2695 prettier 2025-12-17 18:48:38 -05:00
71493aac51 bookclub: Initial commit. Very simple view of all local bookclub messages for now. 2025-12-17 18:47:30 -05:00
8fb1850044 update: CodeMirror. 2025-12-17 18:13:56 -05:00
bbfcbfcae6 ssb: Avoid one last superfluous reload.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m49s
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-12-17 12:38:24 -05:00
cd2903c0df ssb: Embiggen the search input box when focused. 2025-12-17 12:24:58 -05:00
d873d99b23 ssb: Handful of URL encoding issues.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m9s
2025-12-15 20:42:57 -05:00
1a5392d942 ssb: Avoid an unnecessary messages load.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m54s
2025-12-15 12:30:27 -05:00
ef80c0910c intro: Scroll to top when switching pages.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 15m36s
2025-12-13 09:01:17 -05:00
6c641acdd3 ssb: Put the hamburger menu on the same line as the welcome text. 2025-12-13 08:57:06 -05:00
f0babc6f95 core: Fix a recently introduced use after free.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 11m44s
2025-12-12 18:24:23 -05:00
1382eac7e5 ssb: Paranoia around trying to avoid showing stale/irrelevant messages. Need to rethink this approach entirely sometime.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m27s
Build Tilde Friends / Build-All (push) Successful in 9m55s
2025-12-11 22:05:06 -05:00
79b7252a27 ssb: Be much more generous about what's allowed in a hashtag ref. Fixes #dev-diary not behaving correctly as a channel.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-11 22:01:38 -05:00
2e8402d11d update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 11m8s
2025-12-11 12:48:44 -05:00
c34065795c build: Get iOS and Android on the same versionCode/CFBundleVersion.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m2s
2025-12-10 12:34:57 -05:00
1463c18c12 core: Unused.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m29s
2025-12-10 12:28:44 -05:00
f39b0977b7 build: Fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m1s
2025-12-09 21:33:03 -05:00
8f9824e9b7 core: Minor simplification around getting account name.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 3m24s
2025-12-09 20:30:09 -05:00
33392e7c55 core: Move ssb.swapWithServerIdentity() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 20:21:02 -05:00
b4c014fd27 core: Remove some ancient unused resizeMe, setHash, and storeBlob message handlers.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-09 19:05:07 -05:00
81353b4da9 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m8s
2025-12-09 18:48:08 -05:00
d67297c35b ssb: Better search feedback. 2025-12-09 18:43:52 -05:00
192e9e0955 core: Better error handling for deleting users.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m36s
Build Tilde Friends / Build-All (push) Successful in 10m5s
2025-12-09 18:10:47 -05:00
2449202b5d core: Move core.deleteUser() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 18:02:47 -05:00
f1876a34ec core: Move core.globalSettingSet to C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-09 13:02:25 -05:00
3c6eeb9cd3 core: Move invoking the permission test to C, at least for adding/removing blocks.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m56s
Build Tilde Friends / Build-All (push) Successful in 10m15s
2025-12-08 21:51:10 -05:00
c29ab66073 update: c-ares 1.34.6.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Successful in 10m19s
2025-12-08 12:17:50 -05:00
d7782d53a1 build: Nope, guess we needed those deps.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 9m46s
2025-12-07 14:41:02 -05:00
ce3a8c53c6 build: Build docs separately and on a later image. Also remove some build dependencies I don't think we need.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 8m9s
2025-12-07 14:22:09 -05:00
0af54edac1 docs: Add some slight organization.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-07 08:42:34 -05:00
2086075f7b core: Minor cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-07 08:25:55 -05:00
14955fa421 build: #buildfix.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-06 23:14:23 -05:00
1e1059489b test: Disable -t=auto on CI.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m17s
2025-12-06 23:01:07 -05:00
68dc5129c8 build: Let's see what happens if CI tries to run tests.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m23s
2025-12-06 22:43:38 -05:00
690b027c0c core: Remove app.js.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-12-06 20:55:02 -05:00
2f0c379a69 core: Implement websocket timeout in C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m6s
2025-12-06 19:13:06 -05:00
7c1931f529 core: Only the timeout remaing for the websocket handler in C?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m2s
2025-12-06 18:21:42 -05:00
69f9646955 core: Enough websocket in C to run an app. 2025-12-06 18:03:52 -05:00
c4ff00dec1 core: Handle the async process start from the C websocket.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m5s
2025-12-06 15:36:58 -05:00
9ce08f79fb core: Going through the motions of starting a task from C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m59s
2025-12-06 15:17:22 -05:00
0fa9c90ab9 core: Respond with a session message in the C websocket handler. 2025-12-06 14:54:30 -05:00
2b191a5345 core: parentApp hasn't been a thing in a long while. 2025-12-06 14:30:30 -05:00
6381ba6785 core: Need to be able to parse the app path more differently. 2025-12-06 14:26:07 -05:00
d84b06f814 core: Add a helper for getting a property by string as a string. I've typed this too much.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m15s
2025-12-06 14:04:27 -05:00
3a3b889196 core: Inconsistent ready message in editonly mode. 2025-12-06 13:21:45 -05:00
78474e0bea ios: Fix crashes transitioning between apps in one process mode.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m56s
2025-12-06 13:01:23 -05:00
759d5849ba docs: Add rough notes about moving accounts around.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m14s
2025-12-06 12:34:17 -05:00
0df9796fb8 core: Disable some the javascript autocomplete. Breaking tests and my brain.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-06 12:14:58 -05:00
95483b3e55 core: More slight C/websocket progress. 2025-12-06 12:01:26 -05:00
59 changed files with 2222 additions and 1413 deletions

View File

@@ -3,6 +3,36 @@ run-name: ${{ gitea.actor }} running 🚀
on: [push]
jobs:
Build-Docs:
runs-on: ubuntu-latest
container:
image: node:trixie-slim
valid_volumes:
- '/opt/keys'
volumes:
- /opt/keys:/opt/keys
steps:
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
doxygen \
file \
git \
graphviz \
rsync \
unzip \
zip
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make -j`nproc` docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
Build-All:
runs-on: ubuntu-latest
container:
@@ -50,7 +80,6 @@ jobs:
mkdir -p out/html/ ~/.ssh/
make -j`nproc` docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
- name: Setup JDK
uses: actions/setup-java@v3
with:
@@ -60,10 +89,12 @@ jobs:
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018'
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
- name: Test Debug
run: TF_TEST_auto=0 out/debug/tildefriends test
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:

View File

@@ -907,7 +907,6 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched.
INPUT = README.md \
core/app.js \
core/client.js \
core/core.js \
core/tfrpc.js \

View File

@@ -17,7 +17,6 @@ MAKEFLAGS += --no-builtin-rules
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 49
VERSION_CODE_IOS := 27
VERSION_NUMBER := 0.2025.12-wip
VERSION_NAME := This program kills fascists.
@@ -893,7 +892,7 @@ src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
tr '\n' '^' | \
sed -r \
-e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE_IOS)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE)\2@' \
-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
tr '^' '\n' > \
$@.tmp && mv $@.tmp $@ || rm -f $@.tmp

5
apps/bookclub.json Normal file
View File

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

181
apps/bookclub/app.js Normal file
View File

@@ -0,0 +1,181 @@
import * as commonmark from './commonmark.min.js';
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function (row) {
result.push(row);
});
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(`
SELECT
content ->> 'title' AS title,
content ->> '$.image.link' AS image,
content ->> 'description' AS description
FROM messages
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;
}
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>
`);
}
main();

1
apps/bookclub/commonmark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

6
apps/bookclub/index.html Normal file
View File

@@ -0,0 +1,6 @@
<!doctype html>
<html>
<body>
${BODY}
</body>
</html>

251
apps/bookclub/w3.css Normal file
View File

@@ -0,0 +1,251 @@
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-danger{color:#fff!important;background-color:#dd0000!important}
.w3-note{color:#000!important;background-color:#fff599!important}
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
.w3-warning{color:#000!important;background-color:#ffb305!important}
.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

View File

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

View File

@@ -34,6 +34,7 @@
class="w3-flex w3-dark-gray w3-center"
>
<div
id="scrollbox"
style="
flex: 1 1 auto;
overflow: auto;
@@ -251,6 +252,7 @@
index == 0 ? 'hidden' : 'visible';
document.getElementById('right').style.visibility =
index == slides.length - 1 ? 'hidden' : 'visible';
document.getElementById('scrollbox').scrollTo(0, 0);
}
let dots = [...document.getElementsByClassName('dot')];

View File

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

View File

@@ -612,7 +612,7 @@ class TfElement extends LitElement {
by_count.push({count: v.of, id: id});
}
let reactions = this.load_recent_reactions();
this.load_channels_latest(Object.keys(following));
let channels = this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
@@ -625,6 +625,7 @@ class TfElement extends LitElement {
self.users = result;
});
await reactions;
await channels;
this.whoami = whoami;
this.loaded = whoami;
} finally {
@@ -707,9 +708,7 @@ class TfElement extends LitElement {
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
query=${this.search_text()}
></tf-tab-search>
`;
}
@@ -758,7 +757,7 @@ class TfElement extends LitElement {
search_text.focus();
this.set_tab('search');
} else {
this.set_hash('#q=' + search_text.value);
this.set_hash('#q=' + encodeURIComponent(search_text.value));
}
}
@@ -768,6 +767,16 @@ class TfElement extends LitElement {
}
}
search_text() {
if (this.hash.startsWith('#q=')) {
try {
return decodeURIComponent(this.hash.substring('#q='.length));
} catch {
return this.hash.substring('#q='.length);
}
}
}
render() {
let self = this;
@@ -784,6 +793,12 @@ class TfElement extends LitElement {
};
let tabs = html`
<style>
#search_text:focus {
float: none !important;
width: 100%;
}
</style>
<div
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
@@ -832,7 +847,7 @@ class TfElement extends LitElement {
: undefined
}
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>🔍<span class="w3-hide-small">Search</span></button>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown}></input>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown} value=${this.search_text()}></input>
</div>
`;
let contents = this.guest

View File

@@ -648,21 +648,25 @@ class TfMessageElement extends LitElement {
`;
}
contact_description(content) {
return content.following && content.blocking
? 'following and blocking'
: content.following
? 'following'
: content.blocking
? 'blocking'
: content.blocking !== undefined
? 'no longer blocking'
: content.following !== undefined
? 'no longer following'
: '';
}
content_group_by_author() {
let sorted = this.message.messages
.map((x) => [
x.author,
x.content.following && x.content.blocking
? 'is following and blocking'
: x.content.following
? 'is following'
: x.content.blocking
? 'is blocking'
: x.content.blocking !== undefined
? 'is no longer blocking'
: x.content.following !== undefined
? 'is no longer following'
: '',
this.contact_description(x.content),
x.content.contact,
x,
])
@@ -969,16 +973,7 @@ class TfMessageElement extends LitElement {
id=${this.message.author}
.users=${this.users}
></tf-user>
is
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
is ${this.contact_description(content)}
<tf-user
id=${this.message.content.contact}
.users=${this.users}

View File

@@ -57,6 +57,9 @@ class TfNewsElement extends LitElement {
}
function link_message(message) {
if (!message.content) {
return;
}
if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link, message.rowid);
if (!parent.votes) {

View File

@@ -216,6 +216,7 @@ class TfProfileElement extends LitElement {
async load_follows() {
let accounts = await tfrpc.rpc.following([this.id], 1);
delete accounts[this.id];
return html`
<div class="w3-container">
<button
@@ -359,7 +360,7 @@ 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 : '')}>
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')} id="open_private_chat">
Open Private Chat
</a>
${edit}

View File

@@ -398,16 +398,23 @@ class TfTabNewsFeedElement extends LitElement {
);
}
make_messages_key() {
return JSON.stringify([
this.hash,
Object.keys(this.channels_latest ?? {}).filter((x) => x != '🔐'),
]);
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
let original_hash = this.hash;
let original_key = this.make_messages_key();
try {
if (this._messages_hash !== this.hash) {
if (this._messages_key !== original_key) {
this.messages = [];
this._messages_hash = this.hash;
this._messages_key = original_key;
}
this._messages_following = JSON.stringify(this.following);
this._private_messages = JSON.stringify([
@@ -429,7 +436,8 @@ class TfTabNewsFeedElement extends LitElement {
} finally {
this.loading--;
}
if (this.hash == original_hash) {
let current_key = this.make_messages_key();
if (current_key === original_key) {
this.messages = this.merge_messages(this.messages, messages);
}
this.time_loading = undefined;
@@ -485,18 +493,17 @@ class TfTabNewsFeedElement extends LitElement {
render() {
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_key !== this.make_messages_key() ||
this._messages_following !== JSON.stringify(this.following) ||
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]) ||
this._channels_latest !==
JSON.stringify(Object.keys(this.channels_latest))
(this.hash.startsWith('#🔐') &&
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]))
) {
console.log(
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_hash != this.hash} following=${this._messages_following !== JSON.stringify(this.following)}, channels=${this._channels_latest !== JSON.stringify(Object.keys(this.channels_latest))}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
);
this.load_messages();
}

View File

@@ -375,6 +375,14 @@ class TfTabNewsElement extends LitElement {
`;
}
recipients() {
if (this.hash == '#🔐') {
return [this.whoami];
} else if (this.hash.startsWith('#🔐')) {
return this.hash.substring('#🔐'.length).split(',');
}
}
render() {
let profile =
this.hash.startsWith('#@') && this.hash != '#@'
@@ -428,18 +436,18 @@ class TfTabNewsElement extends LitElement {
</p>
<div>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</div>
<span
style="display: inline-block; width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
style="width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
>
<button
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</button>
Welcome,
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</span>
</div>
${edit_profile}
</div>
<div>
@@ -450,9 +458,7 @@ class TfTabNewsElement extends LitElement {
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
.recipients=${this.hash.startsWith('#🔐')
? this.hash.substring('#🔐'.length).split(',')
: undefined}
.recipients=${this.recipients()}
></tf-compose>
</div>
${profile}

View File

@@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js';
@@ -44,36 +44,37 @@ class TfTabSearchElement extends LitElement {
this.error = undefined;
this.results = [];
this.messages = [];
if (query.startsWith('sql:')) {
this.messages = [];
try {
try {
if (query.startsWith('sql:')) {
this.messages = [];
this.results = await tfrpc.rpc.query(
query.substring('sql:'.length),
[]
);
} catch (e) {
this.results = [];
this.error = e;
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
} catch (e) {
this.messages = [];
this.results = [];
this.error = e;
console.log(e);
}
}
@@ -133,17 +134,25 @@ class TfTabSearchElement extends LitElement {
}
}
render() {
async query_results() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
this._query = this.search(this.query);
}
let self = this;
await this._query;
}
render() {
return html`
<style>
${generate_theme()}
</style>
<div class="w3-padding">${this.render_results()}</div>
<div class="w3-padding">
${until(
this.query_results().then(this.render_results.bind(this)),
html`<p>Searching...<span class="w3-animate-fading">🦀</span></p>`
)}
</div>
`;
}
}

View File

@@ -39,7 +39,9 @@ class TfUserElement extends LitElement {
name = this.icon_only
? undefined
: !this.nolink
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
? html`<a target="_top" href=${'#' + encodeURIComponent(this.id)}
>${name_string}</a
>`
: html`<span>${name_string}</span>`;
if (user) {

View File

@@ -1,252 +0,0 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js';
export {App};
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** 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);
}
};
/**
** App socket handler.
** @param request The HTTP request of the WebSocket connection.
** @param response The HTTP response.
*/
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
let credentials = await httpd.auth_query(request.headers);
response.onClose = async function () {
if (process && process.task) {
process.task.kill();
}
if (process) {
process.timeout = 0;
}
};
response.onMessage = async function (event) {
if (event.opCode == 0x1 || event.opCode == 0x2) {
let message;
try {
message = JSON.parse(event.data);
} catch (error) {
print(
'WebSocket error:',
error,
event.data,
event.data.length,
event.opCode
);
return;
}
if (!process && message.action == 'hello') {
let packageOwner;
let packageName;
let blobId;
let match;
let parentApp;
if (
(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
) {
blobId = match[1];
} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
packageOwner = match[1];
packageName = match[2];
blobId = await new Database(packageOwner).get('path:' + packageName);
if (!blobId) {
response.send(
JSON.stringify({
action: 'tfrpc',
method: 'error',
params: [message.path + ' not found'],
id: -1,
}),
0x1
);
return;
}
if (packageOwner != 'core') {
let coreId = await new Database('core').get('path:' + packageName);
parentApp = {
path: '/~core/' + packageName + '/',
id: coreId,
};
}
}
response.send(
JSON.stringify(
Object.assign(
{
action: 'session',
credentials: credentials,
parentApp: parentApp,
id: blobId,
},
await ssb_internal.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName
)
)
),
0x1
);
options.api = message.api || [];
options.credentials = credentials;
options.packageOwner = packageOwner;
options.packageName = packageName;
options.url = message.url;
let sessionId = 'session_' + (g_session_index++).toString();
if (blobId) {
if (message.edit_only) {
response.send(
JSON.stringify({action: 'ready', edit_only: true}),
0x1
);
} else {
process = await core.getProcessBlob(blobId, sessionId, options);
}
}
if (process) {
process.client_api.tfrpc = function (message) {
if (message.id) {
let calls = process?.app?.calls;
if (calls) {
let call = calls[message.id];
if (call) {
if (message.error !== undefined) {
call.reject(message.error);
} else {
call.resolve(message.result);
}
delete calls[message.id];
}
}
}
};
process.app._on_output = (message) =>
response.send(JSON.stringify(message), 0x1);
process.app.send();
}
let ping = function () {
let now = Date.now();
let again = true;
if (now - process.lastActive < process.timeout) {
// Active.
} else if (process.lastPing > process.lastActive) {
// We lost them.
if (process.task) {
process.task.kill();
}
again = false;
} else {
// Idle. Ping them.
response.send('', 0x9);
process.lastPing = now;
}
if (again && process.timeout) {
setTimeout(ping, process.timeout);
}
};
if (process && process.timeout > 0) {
setTimeout(ping, process.timeout);
}
} else {
if (process) {
if (process.client_api[message.action]) {
process.client_api[message.action](message);
} else if (process.eventHandlers['message']) {
await core.invoke(process.eventHandlers['message'], [message]);
}
}
}
} else if (event.opCode == 0x8) {
// Close.
if (process && process.task) {
process.task.kill();
}
response.send(event.data, 0x8);
} else if (event.opCode == 0xa) {
// PONG
}
if (process) {
process.lastActive = Date.now();
}
};
response.upgrade(100, {});
};
/** @} */

View File

@@ -1474,48 +1474,7 @@ function blur() {
* @param event The message.
*/
function message(event) {
if (
event.data &&
event.data.event == 'resizeMe' &&
event.data.width &&
event.data.height
) {
let iframe = document.getElementById('iframe_' + event.data.name);
iframe.setAttribute('width', event.data.width);
iframe.setAttribute('height', event.data.height);
} else if (event.data && event.data.action == 'setHash') {
window.location.hash = event.data.hash;
} else if (event.data && event.data.action == 'storeBlob') {
fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/binary',
},
body: event.data.blob.buffer,
})
.then(function (response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.text();
})
.then(function (text) {
let iframe = document.getElementById('document');
iframe.contentWindow.postMessage(
{
storeBlobComplete: {
name: event.data.blob.name,
path: text,
type: event.data.blob.type,
context: event.data.context,
},
},
'*'
);
});
} else {
send({event: 'message', message: event.data});
}
send({event: 'message', message: event.data});
}
/**

View File

@@ -5,12 +5,6 @@
* @{
*/
/** \cond */
import * as app from './app.js';
export {invoke, getProcessBlob};
/** \endcond */
/** All running processes. */
let gProcesses = {};
/** Whether stats are currently being sent. */
@@ -19,8 +13,6 @@ let gStatsTimer = false;
let g_handler_index = 0;
/** Whether updating accounts information is currently scheduled. */
let g_update_accounts_scheduled;
/** Time between pings, in milliseconds. */
const k_ping_interval = 60 * 1000;
/**
* Print an error.
@@ -167,7 +159,7 @@ function postMessageInternal(from, to, message) {
* @param options Other options.
* @return The process.
*/
async function getProcessBlob(blobId, key, options) {
exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
let process = gProcesses[key];
if (!process && !(options && 'create' in options && !options.create)) {
let resolveReady;
@@ -183,11 +175,59 @@ async function getProcessBlob(blobId, key, options) {
process.url = options?.url;
process.eventHandlers = {};
if (!options?.script || options?.script === 'app.js') {
process.app = new app.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);
}
};
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = k_ping_interval;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@@ -239,8 +279,8 @@ async function getProcessBlob(blobId, key, options) {
} else {
throw Error(`Permission denied: ${permission}.`);
}
} else if (process.app) {
return process.app
} else if (process.makeFunction) {
return process
.makeFunction(['requestPermission'])(permission, description)
.then(async function (value) {
if (value == 'allow') {
@@ -284,7 +324,7 @@ 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',
@@ -342,39 +382,11 @@ async function getProcessBlob(blobId, key, options) {
throw new Error('Must be signed-in to create an account.');
}
};
if (process.credentials?.permissions?.administration) {
imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest(
'set_global_setting',
`Set ${JSON.stringify(key)} to ${JSON.stringify(value)}.`
);
print('Setting', key, value);
let settings = await loadSettings();
settings[key] = value;
await new Database('core').set('settings', JSON.stringify(settings));
print('Done.');
};
imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user');
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = await db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
await db.set('users', users);
}
};
}
if (options.api) {
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 || {})) {
@@ -387,8 +399,8 @@ async function getProcessBlob(blobId, key, options) {
};
process.task.onError = function (error) {
try {
if (process.app) {
process.app.makeFunction(['error'])(error);
if (process.makeFunction) {
process.makeFunction(['error'])(error);
} else {
printError(error);
}
@@ -467,57 +479,6 @@ async function getProcessBlob(blobId, key, options) {
});
}
};
imports.ssb.privateMessageEncrypt = function (id, recipients, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageEncrypt(
process.credentials.session.name,
id,
recipients,
message
);
}
};
imports.ssb.privateMessageDecrypt = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageDecrypt(
process.credentials.session.name,
id,
message
);
}
};
if (process.credentials?.permissions?.administration) {
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.swapWithServerIdentity(
process.credentials.session.name,
id
);
}
};
imports.ssb.addBlock = async function (id) {
await imports.core.permissionTest('modify_blocks', `Block ${id}.`);
await ssb_internal.addBlock(id);
};
imports.ssb.removeBlock = async function (id) {
await imports.core.permissionTest('modify_blocks', `Unblock ${id}.`);
await ssb_internal.removeBlock(id);
};
imports.ssb.getBlocks = ssb_internal.getBlocks.bind(ssb_internal);
}
if (
process.credentials &&
process.credentials.session &&
@@ -567,7 +528,7 @@ async function getProcessBlob(blobId, key, options) {
};
}
process.sendPermissions = async function sendPermissions() {
process.app.send({
process.send({
action: 'permissions',
permissions: await imports.core.permissionsGranted(),
});
@@ -592,6 +553,7 @@ async function getProcessBlob(blobId, key, options) {
},
};
ssb.registerImports(imports, process);
process.imports = imports;
process.task.setImports(imports);
process.task.activate();
let source = await ssb.blobGet(blobId);
@@ -620,8 +582,8 @@ async function getProcessBlob(blobId, key, options) {
} catch (e) {
printError(e);
}
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});
@@ -640,7 +602,7 @@ async function getProcessBlob(blobId, key, options) {
}
}
return process;
}
};
/**
* Send any changed account information.
@@ -704,13 +666,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 {

2
deps/c-ares vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import {EditorState, Compartment} from "@codemirror/state"
import {EditorView} from '@codemirror/view';
import {javascript} from "@codemirror/lang-javascript"
import {javascriptLanguage} from "@codemirror/lang-javascript"
import {htmlLanguage, html} from "@codemirror/lang-html"
import {css} from "@codemirror/lang-css"
import {markdown} from "@codemirror/lang-markdown"
@@ -23,20 +23,6 @@ let updateListenerExtension = EditorView.updateListener.of((update) => {
/* https://codemirror.net/examples/config/ */
const languageConfig = new Compartment();
const autoLanguage = EditorState.transactionExtender.of(tr => {
if (!tr.docChanged) {
return null;
}
let doc_is_html = /\s*</.test(tr.newDoc.sliceString(0, 100));
let state_is_html = tr.startState.facet(language) == htmlLanguage;
if (doc_is_html == state_is_html) {
return null;
}
return {
effects: languageConfig.reconfigure(doc_is_html ? html() : javascript()),
};
});
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
@@ -66,8 +52,7 @@ const extensions = [
...lintKeymap,
indentWithTab,
]),
languageConfig.of(javascript()),
autoLanguage,
languageConfig.of(javascriptLanguage),
search(),
oneDark,
updateListenerExtension,
@@ -84,7 +69,7 @@ function setEditorMode(view, mode) {
const k_modes = {
'css': css(),
'html': html(),
'javascript': javascript(),
'javascript': javascriptLanguage,
'markdown': markdown(),
'xml': xml(),
};

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

@@ -33,9 +33,9 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
"integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz",
"integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -186,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.8",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"version": "6.39.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -307,18 +307,18 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.4.tgz",
"integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.0.tgz",
"integrity": "sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==",
"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==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
@@ -412,9 +412,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"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==",
"cpu": [
"arm"
],
@@ -425,9 +425,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
"cpu": [
"arm64"
],
@@ -438,9 +438,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"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==",
"cpu": [
"arm64"
],
@@ -451,9 +451,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"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==",
"cpu": [
"x64"
],
@@ -464,9 +464,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"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==",
"cpu": [
"arm64"
],
@@ -477,9 +477,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"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==",
"cpu": [
"x64"
],
@@ -490,9 +490,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"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==",
"cpu": [
"arm"
],
@@ -503,9 +503,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"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==",
"cpu": [
"arm"
],
@@ -516,9 +516,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"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==",
"cpu": [
"arm64"
],
@@ -529,9 +529,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"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==",
"cpu": [
"arm64"
],
@@ -542,9 +542,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"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==",
"cpu": [
"loong64"
],
@@ -555,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"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==",
"cpu": [
"ppc64"
],
@@ -568,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"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==",
"cpu": [
"riscv64"
],
@@ -581,9 +581,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"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==",
"cpu": [
"riscv64"
],
@@ -594,9 +594,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"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==",
"cpu": [
"s390x"
],
@@ -607,9 +607,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"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==",
"cpu": [
"x64"
],
@@ -620,9 +620,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"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==",
"cpu": [
"x64"
],
@@ -633,9 +633,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
"cpu": [
"arm64"
],
@@ -646,9 +646,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"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==",
"cpu": [
"arm64"
],
@@ -659,9 +659,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"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==",
"cpu": [
"ia32"
],
@@ -672,9 +672,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"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==",
"cpu": [
"x64"
],
@@ -685,9 +685,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"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==",
"cpu": [
"x64"
],
@@ -877,9 +877,9 @@
}
},
"node_modules/rollup": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -892,28 +892,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.3",
"@rollup/rollup-android-arm64": "4.53.3",
"@rollup/rollup-darwin-arm64": "4.53.3",
"@rollup/rollup-darwin-x64": "4.53.3",
"@rollup/rollup-freebsd-arm64": "4.53.3",
"@rollup/rollup-freebsd-x64": "4.53.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
"@rollup/rollup-linux-arm64-musl": "4.53.3",
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
"@rollup/rollup-linux-x64-gnu": "4.53.3",
"@rollup/rollup-linux-x64-musl": "4.53.3",
"@rollup/rollup-openharmony-arm64": "4.53.3",
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
"@rollup/rollup-win32-x64-gnu": "4.53.3",
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"@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",
"fsevents": "~2.3.2"
}
},

4
docs/app_development.md Normal file
View File

@@ -0,0 +1,4 @@
@page app_development App Development
- @subpage app_development_cheat_sheet
- @subpage app_development_guide

View File

@@ -1,4 +1,4 @@
# App Development Cheat Sheet
@page app_development_cheat_sheet App Development Cheat Sheet
Making apps for the impatient tilde friend.

View File

@@ -1,4 +1,4 @@
# App Development Guide
@page app_development_guide App Development Guide
A Tilde Friends application starts with code that runs on a Tilde Friends server, possibly far away from where you wrote it, in a little JavaScript environment, in its own restricted process, with the only access to the outside world being the ability to send messages to the server. This document gives some recipes showing how that can be used to build a functional user-facing application in light of the unique constraints present.

View File

@@ -1,3 +1,5 @@
@page connecting_manyverse How to Connect Manyverse
# Connecting with Manyverse
Communication with [Manyverse](https://www.manyver.se/) should Just Work (tm).

5
docs/howto.md Normal file
View File

@@ -0,0 +1,5 @@
@page howto How To
- @subpage upgrading
- @subpage transfer_account
- @subpage connecting_manyverse

View File

@@ -1,4 +1,4 @@
# Inspiration
@page inspiration Inspiration
This is an ever-growing list of software that is similar to what Tilde Friends tries to be but as far as I can tell don't quite fit the same niche.

View File

@@ -1,4 +1,4 @@
# Model
@page model Model
A reasonable mental model of Tilde Friends is as a virtual computer. User
interace is through a web browser. Communication with the outside world is

5
docs/overview.md Normal file
View File

@@ -0,0 +1,5 @@
@page overview Overview
- @subpage inspiration
- @subpage model
- @subpage vision

View File

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

18
docs/transfer_account.md Normal file
View File

@@ -0,0 +1,18 @@
@page transfer_account How to Transfer an Account
Secure Scuttlebutt accounts can be easily transferred between apps and devices.
However, it is not recommended to use an account on multiple devices. If you
author a message on one device without having received all messages authored
from another, your account may become irrecoverably forked. Other clients may
stop receiving your messages if this happens.
1. In Tilde Friends, the _identity_ app will let you export and import your identity as a series of twelve English words for copying and pasting. Keep these words secret!
2. The _sneaker_ app will let you export and import your feed to a file, but it's likely easier and faster to use your initial account on the receiver as a throwaway account to connect, follow yourself, and do the initial replication.
Your identity and messages on the target device is all you need to resume posting. Deleting the identity from the source to avoid accidentally using it is probably a wise idea.
If you are moving accounts between applications on the same device, note that they may not be able to see each other if they attempt to use the same network port number.
You may also wish to make your account the server identity in the _identity_ app so that other devices on the network see and connect to you by your well-known identity.

View File

@@ -1,4 +1,4 @@
# Upgrading
@page upgrading Upgrading
Tilde Friends can be upgraded simply by running a new executable against an
existing database.

View File

@@ -1,4 +1,4 @@
# Vision
@page vision Vision
Tilde Friends is a tool for making and sharing.

View File

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

6
package-lock.json generated
View File

@@ -11,9 +11,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

View File

@@ -7,7 +7,22 @@
#include "task.h"
#include "util.js.h"
#include <quickjs.h>
#include "quickjs.h"
#include "sodium/crypto_box.h"
#include "sodium/crypto_scalarmult.h"
#include "sodium/crypto_scalarmult_curve25519.h"
#include "sodium/crypto_scalarmult_ed25519.h"
#include "sodium/crypto_secretbox.h"
#include "sodium/crypto_sign.h"
#include "sodium/randombytes.h"
#include "sqlite3.h"
#include <assert.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _app_path_pair_t
{
@@ -91,40 +106,37 @@ static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_d
tf_free(work);
}
static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
JSValue name_value = JS_IsObject(session) ? JS_GetPropertyStr(context, session, "name") : JS_UNDEFINED;
const char* result = JS_IsString(name_value) ? JS_ToCString(context, name_value) : NULL;
JS_FreeValue(context, name_value);
JS_FreeValue(context, session);
JS_FreeValue(context, credentials);
return result;
}
static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue user = argv[0];
JSValue process = data[0];
const char* user_string = JS_IsString(user) ? JS_ToCString(context, user) : NULL;
const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
if (JS_IsObject(process))
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
if (JS_IsObject(credentials))
{
JSValue session = JS_GetPropertyStr(context, credentials, "session");
if (JS_IsObject(session))
{
JSValue session_name = JS_GetPropertyStr(context, session, "name");
const char* session_name_string = JS_IsString(session_name) ? JS_ToCString(context, session_name) : NULL;
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
JS_FreeValue(context, session_name);
}
JS_FreeValue(context, session);
}
JS_FreeValue(context, credentials);
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
if (user_string)
{
@@ -374,26 +386,12 @@ static void _tf_api_core_permissions_granted_after_work(tf_ssb_t* ssb, int statu
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->user);
JS_FreeCString(context, work->user);
tf_free((void*)work->package_owner);
tf_free((void*)work->package_name);
tf_free(work);
}
static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
JSValue name_value = JS_IsObject(session) ? JS_GetPropertyStr(context, session, "name") : JS_UNDEFINED;
const char* name = JS_IsString(name_value) ? JS_ToCString(context, name_value) : NULL;
const char* result = tf_strdup(name);
JS_FreeCString(context, name);
JS_FreeValue(context, name_value);
JS_FreeValue(context, session);
JS_FreeValue(context, credentials);
return result;
}
static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
@@ -502,7 +500,7 @@ static JSValue _tf_ssb_getActiveIdentity(JSContext* context, JSValueConst this_v
.package_name = tf_strdup(package_name),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_free((void*)name);
JS_FreeCString(context, name);
JS_FreeCString(context, package_owner);
JS_FreeCString(context, package_name);
@@ -605,7 +603,7 @@ static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val,
.context = context,
};
memcpy(work->user, user, user_length + 1);
tf_free((void*)user);
JS_FreeCString(context, user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
@@ -796,6 +794,743 @@ static JSValue _tf_ssb_globalSettingsGet(JSContext* context, JSValueConst this_v
return result;
}
typedef struct _modify_block_t
{
const char* user;
char id[k_id_base64_len];
bool add;
bool completed;
JSValue result;
JSValue promise[2];
} modify_block_t;
static void _tf_ssb_modify_block_work(tf_ssb_t* ssb, void* user_data)
{
modify_block_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (work->add)
{
tf_ssb_db_add_block(db, work->id);
}
else
{
tf_ssb_db_remove_block(db, work->id);
}
tf_ssb_release_db_writer(ssb, db);
work->completed = true;
}
static void _tf_ssb_modify_block_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
modify_block_t* request = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_Call(context, request->completed ? request->promise[0] : request->promise[1], JS_UNDEFINED, 1, &request->result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
JS_FreeValue(context, request->result);
JS_FreeCString(context, request->user);
tf_free(request);
}
typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSValue value, void* user_data);
typedef struct _permission_test_t
{
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]);
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]);
work->callback(context, false, argv[0], work->user_data);
tf_free(work);
return JS_UNDEFINED;
}
static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data)
{
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
*payload = (permission_test_t) {
.callback = callback,
.user_data = user_data,
};
JSValue opaque = JS_NewObject(context);
JS_SetOpaque(opaque, payload);
JSValue imports = JS_GetPropertyStr(context, process, "imports");
JSValue core = JS_GetPropertyStr(context, imports, "core");
JSValue permission_test = JS_GetPropertyStr(context, core, "permissionTest");
JSValue args[] = {
JS_NewString(context, permission),
JS_NewString(context, description),
};
JSValue promise = JS_Call(context, permission_test, imports, tf_countof(args), args);
JSValue then = JS_GetPropertyStr(context, promise, "then");
JSValue catch = JS_GetPropertyStr(context, promise, "catch");
JSValue resolve = JS_NewCFunctionData(context, _tf_ssb_permission_test_resolve, 1, 0, 1, &opaque);
JSValue reject = JS_NewCFunctionData(context, _tf_ssb_permission_test_reject, 1, 0, 1, &opaque);
JSValue result = JS_Call(context, then, promise, 1, &resolve);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
result = JS_Call(context, catch, promise, 1, &reject);
tf_util_report_error(context, result);
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++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, permission_test);
JS_FreeValue(context, core);
JS_FreeValue(context, imports);
}
static void _tf_ssb_modify_block_start_work(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);
modify_block_t* work = user_data;
work->result = JS_DupValue(context, value);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
}
else
{
_tf_ssb_modify_block_after_work(ssb, 0, work);
}
}
static JSValue _tf_ssb_add_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) {
.user = _tf_ssb_get_process_credentials_session_name(context, data[0]),
.add = true,
};
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
char description[256] = "";
snprintf(description, sizeof(description), "Block %s.", work->id);
_tf_ssb_permission_test(context, data[0], "modify_blocks", description, _tf_ssb_modify_block_start_work, work);
return result;
}
static JSValue _tf_ssb_remove_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) {
.user = _tf_ssb_get_process_credentials_session_name(context, data[0]),
.add = false,
};
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
char description[256] = "";
snprintf(description, sizeof(description), "Unblock %s.", work->id);
_tf_ssb_permission_test(context, data[0], "modify_blocks", description, _tf_ssb_modify_block_start_work, work);
return result;
}
typedef struct _block_t
{
char id[k_id_base64_len];
double timestamp;
} block_t;
typedef struct _get_blocks_t
{
block_t* blocks;
int count;
JSValue promise[2];
} get_blocks_t;
static void _get_blocks_callback(const char* id, double timestamp, void* user_data)
{
get_blocks_t* work = user_data;
work->blocks = tf_resize_vec(work->blocks, sizeof(block_t) * (work->count + 1));
work->blocks[work->count] = (block_t) { .timestamp = timestamp };
tf_string_set(work->blocks[work->count].id, sizeof(work->blocks[work->count].id), id);
work->count++;
}
static void _tf_ssb_get_blocks_work(tf_ssb_t* ssb, void* user_data)
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_blocks(db, _get_blocks_callback, user_data);
tf_ssb_release_db_reader(ssb, db);
}
static void _tf_ssb_get_blocks_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_blocks_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JSValue entry = JS_NewObject(context);
JS_SetPropertyStr(context, entry, "id", JS_NewString(context, work->blocks[i].id));
JS_SetPropertyStr(context, entry, "timestamp", JS_NewFloat64(context, work->blocks[i].timestamp));
JS_SetPropertyUint32(context, result, i, entry);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->blocks);
tf_free(work);
}
static JSValue _tf_ssb_get_blocks(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);
get_blocks_t* work = tf_malloc(sizeof(get_blocks_t));
*work = (get_blocks_t) { 0 };
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_blocks_work, _tf_ssb_get_blocks_after_work, work);
return result;
}
typedef struct _global_setting_set_t
{
const char* key;
const char* value;
JSValue promise[2];
bool done;
JSValue result;
} global_setting_set_t;
static void _tf_ssb_globalSettingsSet_work(tf_ssb_t* ssb, void* user_data)
{
global_setting_set_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
work->done = tf_ssb_db_set_global_setting_from_string(db, work->key, work->value);
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_globalSettingsSet_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
global_setting_set_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_Call(context, work->done ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &work->result);
JS_FreeValue(context, work->result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->key);
JS_FreeCString(context, work->value);
tf_free(work);
}
static void _tf_ssb_globalSettingsSet_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
global_setting_set_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
work->result = value;
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_globalSettingsSet_work, _tf_ssb_globalSettingsSet_after_work, work);
}
else
{
_tf_ssb_globalSettingsSet_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_globalSettingsSet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
const char* key = JS_ToCString(context, argv[0]);
const char* value = JS_ToCString(context, argv[1]);
global_setting_set_t* work = tf_malloc(sizeof(global_setting_set_t));
*work = (global_setting_set_t) {
.key = key,
.value = value,
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
char description[256] = "";
snprintf(description, sizeof(description), "Set %s to %s.", key, value);
_tf_ssb_permission_test(context, data[0], "set_global_setting", description, _tf_ssb_globalSettingsSet_permission_callback, work);
return result;
}
typedef struct _delete_user_t
{
const char* user;
bool completed;
JSValue result;
JSValue promise[2];
} delete_user_t;
static void _tf_ssb_delete_user_work(tf_ssb_t* ssb, void* user_data)
{
delete_user_t* work = user_data;
size_t length = strlen("user:") + strlen(work->user) + 1;
char* buffer = alloca(length);
snprintf(buffer, length, "user:%s", work->user);
work->completed = tf_ssb_db_remove_property(ssb, "auth", buffer) || work->completed;
work->completed = tf_ssb_db_remove_value_from_array_property(ssb, "auth", "users", work->user) || work->completed;
}
static void _tf_ssb_delete_user_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_user_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
if (!work->completed && JS_IsUndefined(work->result))
{
work->result = JS_NewString(context, "User not found.");
}
JSValue error = JS_Call(context, work->completed ? work->promise[0] : work->promise[1], JS_UNDEFINED, 1, &work->result);
JS_FreeValue(context, work->result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
tf_free(work);
}
static void _tf_ssb_delete_user_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
delete_user_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_delete_user_work, _tf_ssb_delete_user_after_work, work);
}
else
{
_tf_ssb_delete_user_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_delete_user(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
delete_user_t* work = tf_malloc(sizeof(delete_user_t));
*work = (delete_user_t) {
.user = JS_ToCString(context, argv[0]),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
char description[256] = "";
snprintf(description, sizeof(description), "Delete user '%s'.", work->user);
_tf_ssb_permission_test(context, data[0], "delete_user", description, _tf_ssb_delete_user_permission_callback, work);
return result;
}
typedef struct _swap_with_server_identity_t
{
char server_id[k_id_base64_len];
char user_id[k_id_base64_len];
JSValue promise[2];
char* error;
char user[];
} swap_with_server_identity_t;
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
swap_with_server_identity_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
work->error = tf_ssb_db_swap_with_server_identity(db, work->user, work->user_id, work->server_id);
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
swap_with_server_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
JSValue exception = JS_GetException(context);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
tf_free(work->error);
JS_FreeValue(context, exception);
JS_FreeValue(context, arg);
}
else
{
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static void _tf_ssb_swap_with_server_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
swap_with_server_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
}
static JSValue _tf_ssb_swap_with_server_identity(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);
const char* user = _tf_ssb_get_process_credentials_session_name(context, data[0]);
size_t user_length = user ? strlen(user) : 0;
const char* id = JS_ToCString(context, argv[0]);
swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
*work = (swap_with_server_identity_t) { 0 };
tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
tf_string_set(work->user_id, sizeof(work->user_id), id);
if (user)
{
memcpy(work->user, user, user_length + 1);
}
else
{
*work->user = '\0';
}
JSValue result = JS_NewPromiseCapability(context, work->promise);
char description[1024];
snprintf(description, sizeof(description), "Swap identity %s with %s.", work->user_id, work->server_id);
_tf_ssb_permission_test(context, data[0], "delete_user", description, _tf_ssb_swap_with_server_identity_permission_callback, work);
JS_FreeCString(context, id);
JS_FreeCString(context, user);
return result;
}
static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (!user || !identity)
{
tf_printf("user=%p identity=%p out_private_key=%p\n", user, identity, out_private_key);
return false;
}
bool success = false;
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "SELECT private_key FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, *identity == '@' ? identity + 1 : identity, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
uint8_t key[crypto_sign_SECRETKEYBYTES] = { 0 };
int length = tf_base64_decode((const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), key, sizeof(key));
if (length == crypto_sign_SECRETKEYBYTES)
{
success = crypto_sign_ed25519_sk_to_curve25519(out_private_key, key) == 0;
}
}
}
sqlite3_finalize(statement);
}
return success;
}
static bool _tf_ssb_get_private_key_curve25519(tf_ssb_t* ssb, sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (_tf_ssb_get_private_key_curve25519_internal(db, user, identity, out_private_key))
{
return true;
}
if (tf_ssb_db_user_has_permission(ssb, db, user, "administration"))
{
return _tf_ssb_get_private_key_curve25519_internal(db, ":admin", identity, out_private_key);
}
return false;
}
typedef struct _private_message_encrypt_t
{
const char* signer_user;
const char* signer_identity;
const char* recipients[k_max_private_message_recipients];
int recipient_count;
const char* message;
size_t message_size;
JSValue promise[2];
bool error_id_not_found;
char* encrypted;
size_t encrypted_length;
} private_message_encrypt_t;
static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_encrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->signer_user, work->signer_identity, private_key);
tf_ssb_release_db_reader(ssb, db);
if (found)
{
work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size);
work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0;
}
else
{
work->error_id_not_found = true;
}
}
static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
private_message_encrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
bool success = false;
if (work->error_id_not_found)
{
result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", work->signer_identity, work->signer_user);
}
else if (!work->encrypted)
{
result = JS_ThrowInternalError(context, "Encrypt failed.");
}
else
{
result = JS_NewStringLen(context, work->encrypted, work->encrypted_length);
tf_free((void*)work->encrypted);
success = true;
}
for (int i = 0; i < work->recipient_count; i++)
{
tf_free((void*)work->recipients[i]);
}
JSValue error = JS_Call(context, work->promise[success ? 0 : 1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->signer_user);
JS_FreeCString(context, work->signer_identity);
JS_FreeCString(context, work->message);
tf_free(work);
}
static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue process = data[0];
int recipient_count = tf_util_get_length(context, argv[1]);
if (recipient_count < 1 || recipient_count > k_max_private_message_recipients)
{
return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients);
}
const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
char* recipients[k_max_private_message_recipients] = { 0 };
for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
{
JSValue recipient = JS_GetPropertyUint32(context, argv[1], i);
const char* id = JS_ToCString(context, recipient);
if (id)
{
recipients[i] = tf_strdup(id);
JS_FreeCString(context, id);
}
JS_FreeValue(context, recipient);
}
if (JS_IsUndefined(result))
{
const char* signer_identity = JS_ToCString(context, argv[0]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[2]);
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
private_message_encrypt_t* work = tf_malloc(sizeof(private_message_encrypt_t));
*work = (private_message_encrypt_t) {
.signer_user = session_name_string,
.signer_identity = signer_identity,
.recipient_count = recipient_count,
.message = message,
.message_size = message_size,
};
static_assert(sizeof(work->recipients) == sizeof(recipients), "size mismatch");
memcpy(work->recipients, recipients, sizeof(recipients));
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_private_message_encrypt_work, _tf_ssb_private_message_encrypt_after_work, work);
}
return result;
}
typedef struct _private_message_decrypt_t
{
const char* user;
const char* identity;
size_t message_size;
const char* message;
const char* decrypted;
size_t decrypted_size;
const char* error;
JSValue promise[2];
} private_message_decrypt_t;
static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_decrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->user, work->identity, private_key);
tf_ssb_release_db_reader(ssb, db);
if (found)
{
if (work->message_size >= strlen(".box") && memcmp(work->message + work->message_size - strlen(".box"), ".box", strlen(".box")) == 0)
{
uint8_t* decoded = tf_malloc(work->message_size);
int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size);
uint8_t* nonce = decoded;
uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
{
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
{
enum
{
k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
};
for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes;
p += k_recipient_header_bytes)
{
uint8_t out[k_recipient_header_bytes] = { 0 };
int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
if (opened != -1)
{
int recipients = (int)out[0];
uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
size_t body_size = decoded + decoded_length - body;
uint8_t* decrypted = tf_malloc(body_size);
uint8_t* key = out + 1;
if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
{
work->decrypted = (const char*)decrypted;
work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
}
else
{
work->error = "Received key to open secret box containing message body, but it did not work.";
}
}
}
}
else
{
work->error = "crypto_scalarmult failed.";
}
}
else
{
work->error = "Encrypted message was not long enough to contain its one-time public key.";
}
tf_free(decoded);
}
else
{
work->error = "Message does not end in \".box\".";
}
}
else
{
work->error = "Private key not found for user.";
}
}
static void _tf_ssb_private_message_decrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
private_message_decrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue result = JS_ThrowInternalError(context, "%s", work->error);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else if (work->decrypted)
{
JSValue result = JS_NewStringLen(context, work->decrypted, work->decrypted_size);
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else
{
JSValue result = JS_UNDEFINED;
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->identity);
JS_FreeCString(context, work->message);
tf_free((void*)work->decrypted);
tf_free(work);
}
static JSValue _tf_ssb_private_message_decrypt(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];
const char* identity = JS_ToCString(context, argv[0]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[1]);
const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
private_message_decrypt_t* work = tf_malloc(sizeof(private_message_decrypt_t));
*work = (private_message_decrypt_t) {
.user = session_name_string,
.identity = identity,
.message_size = message_size,
.message = message,
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_private_message_decrypt_work, _tf_ssb_private_message_decrypt_after_work, work);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -822,6 +1557,8 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, ssb, "getActiveIdentity", JS_NewCFunctionData(context, _tf_ssb_getActiveIdentity, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getIdentities", JS_NewCFunctionData(context, _tf_ssb_getIdentities, 0, 0, 1, &process));
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_FreeValue(context, ssb);
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
@@ -831,6 +1568,15 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
{
JS_SetPropertyStr(context, core, "globalSettingsDescriptions", JS_NewCFunction(context, _tf_ssb_globalSettingsDescriptions, "globalSettingsDescriptions", 0));
JS_SetPropertyStr(context, core, "globalSettingsGet", JS_NewCFunction(context, _tf_ssb_globalSettingsGet, "globalSettingsGet", 1));
JS_SetPropertyStr(context, core, "globalSettingsSet", JS_NewCFunctionData(context, _tf_ssb_globalSettingsSet, 2, 0, 1, &process));
JS_SetPropertyStr(context, core, "deleteUser", JS_NewCFunctionData(context, _tf_ssb_delete_user, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "addBlock", JS_NewCFunctionData(context, _tf_ssb_add_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "removeBlock", JS_NewCFunctionData(context, _tf_ssb_remove_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getBlocks", JS_NewCFunctionData(context, _tf_ssb_get_blocks, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "swapWithServerIdentity", JS_NewCFunctionData(context, _tf_ssb_swap_with_server_identity, 1, 0, 1, &process));
}
JS_FreeValue(context, administration);
JS_FreeValue(context, permissions);

View File

@@ -331,7 +331,7 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
{
if (size)
{
connection->body = tf_resize_vec(connection->body, connection->body_length + size);
connection->body = tf_resize_vec(connection->body, connection->body_length + size + 1);
memcpy((char*)connection->body + connection->body_length, data, size);
connection->body_length += size;
}
@@ -385,7 +385,7 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
if (!fin || connection->fragment_length)
{
connection->fragment = tf_resize_vec(connection->fragment, connection->fragment_length + length);
connection->fragment = tf_resize_vec(connection->fragment, connection->fragment_length + length + 1);
memcpy((uint8_t*)connection->fragment + connection->fragment_length, message, length);
connection->fragment_length += length;
}
@@ -394,10 +394,14 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
{
if (connection->request && connection->request->on_message)
{
uint8_t* payload = connection->fragment_length ? connection->fragment : message;
size_t payload_length = connection->fragment_length ? connection->fragment_length : length;
uint8_t backup = payload[payload_length];
payload[payload_length] = '\0';
tf_trace_begin(connection->http->trace, connection->trace_name ? connection->trace_name : "websocket");
connection->request->on_message(connection->request, connection->fragment_length ? connection->fragment_op_code : op_code,
connection->fragment_length ? connection->fragment : message, connection->fragment_length ? connection->fragment_length : length);
connection->request->on_message(connection->request, connection->fragment_length ? connection->fragment_op_code : op_code, payload, payload_length);
tf_trace_end(connection->http->trace);
payload[payload_length] = backup;
}
connection->fragment_length = 0;
}
@@ -946,11 +950,19 @@ void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, con
copy[9] = (low >> 0) & 0xff;
header += 9;
}
memcpy(copy + header, data, size);
if (size)
{
memcpy(copy + header, data, size);
}
_http_write(request->connection, copy, header + size);
tf_free(copy);
}
void tf_http_request_websocket_close(tf_http_request_t* request)
{
_http_connection_destroy(request->connection, "websocket close");
}
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length)
{
if (request->connection->is_response_sent)

View File

@@ -210,6 +210,13 @@ const char* tf_http_get_cookie(const char* cookie_header, const char* name);
*/
void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, const void* data, size_t size);
/**
** Close a websocket.
** @param request The HTTP request which was previously updated to a websocket
** session with tf_http_request_websocket_upgrade().
*/
void tf_http_request_websocket_close(tf_http_request_t* request);
/**
** Upgrade an HTTP request to a websocket session.
** @param request The HTTP request.

View File

@@ -218,10 +218,14 @@ void tf_httpd_endpoint_app(tf_http_request_t* request)
typedef struct _app_t
{
tf_http_request_t* request;
uv_timer_t timer;
const char* settings;
JSValue opaque;
JSValue credentials;
tf_taskstub_t* taskstub;
JSValue process;
uint64_t last_ping_ms;
uint64_t last_active_ms;
bool got_hello;
} app_t;
static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
@@ -230,25 +234,379 @@ static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _httpd_app_kill_task(app_t* work)
{
JSContext* context = work->request->context;
if (JS_IsObject(work->process))
{
JSValue task = JS_GetPropertyStr(context, work->process, "task");
if (JS_IsObject(task))
{
JSValue kill = JS_GetPropertyStr(context, task, "kill");
if (!JS_IsUndefined(kill))
{
JSValue result = JS_Call(context, kill, task, 0, NULL);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, kill);
}
}
JS_FreeValue(context, task);
}
}
typedef struct _app_hello_t
{
app_t* app;
JSValue message;
const char* user;
const char* path;
char blob_id[k_id_base64_len];
tf_ssb_identity_info_t* identity_info;
tf_httpd_user_app_t* user_app;
} app_hello_t;
static void _httpd_app_hello_work(tf_ssb_t* ssb, void* user_data)
{
app_hello_t* work = user_data;
work->user_app = tf_httpd_parse_user_app_from_path(work->path, NULL);
if (work->user_app)
{
size_t length = strlen("path:") + strlen(work->user_app->app) + 1;
char* key = alloca(length);
snprintf(key, length, "path:%s", work->user_app->app);
const char* value = tf_ssb_db_get_property(ssb, work->user_app->user, key);
tf_string_set(work->blob_id, sizeof(work->blob_id), value);
tf_free((void*)value);
}
else if (work->path[0] == '/' && (work->path[1] == '%' || work->path[1] == '&') && strlen(work->path) >= 1 + k_blob_id_len && strstr(work->path, ".sha256"))
{
memcpy(work->blob_id, work->path + 1, strstr(work->path, ".sha256") - work->path - 1 + strlen(".sha256"));
}
if (*work->blob_id)
{
work->identity_info = tf_ssb_db_get_identity_info(ssb, work->user, work->user_app ? work->user_app->user : NULL, work->user_app ? work->user_app->app : NULL);
}
}
static void _http_json_send(tf_http_request_t* request, JSContext* context, JSValue value)
{
JSValue json = JS_JSONStringify(context, value, JS_NULL, JS_NULL);
size_t json_length = 0;
const char* payload = JS_ToCStringLen(context, &json_length, json);
tf_http_request_websocket_send(request, 0x1, payload, json_length);
JS_FreeCString(context, payload);
JS_FreeValue(context, json);
}
static JSValue _httpd_app_on_tfrpc(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
const char* id = tf_util_get_property_as_string(context, argv[0], "id");
if (id)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
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))
{
JSValue error = JS_GetPropertyStr(context, argv[0], "error");
if (!JS_IsUndefined(error))
{
JSValue reject = JS_GetPropertyStr(context, call, "reject");
JSValue result = JS_Call(context, reject, JS_UNDEFINED, 1, &error);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, reject);
}
else
{
JSValue resolve = JS_GetPropertyStr(context, call, "resolve");
JSValue message_result = JS_GetPropertyStr(context, argv[0], "result");
JSValue result = JS_Call(context, resolve, JS_UNDEFINED, 1, &message_result);
JS_FreeValue(context, message_result);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, resolve);
}
JS_FreeValue(context, error);
}
JS_FreeValue(context, call);
JS_FreeValue(context, calls);
}
JS_FreeCString(context, id);
return JS_UNDEFINED;
}
static JSValue _httpd_app_on_output(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
if (app)
{
_http_json_send(app->request, context, argv[0]);
}
return JS_UNDEFINED;
}
static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
app->process = JS_DupValue(context, argv[0]);
JSValue client_api = JS_GetPropertyStr(context, app->process, "client_api");
JSValue tfrpc = JS_NewCFunctionData(context, _httpd_app_on_tfrpc, 1, 0, 1, func_data);
JS_SetPropertyStr(context, client_api, "tfrpc", tfrpc);
JS_FreeValue(context, client_api);
JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
JS_SetPropertyStr(context, app->process, "_on_output", on_output);
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);
return JS_UNDEFINED;
}
static void _httpd_app_hello_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_hello_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
if (!*work->blob_id)
{
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "action", JS_NewString(context, "tfrpc"));
JS_SetPropertyStr(context, object, "method", JS_NewString(context, "error"));
JSValue params = JS_NewArray(context);
size_t length = strlen(work->path) + strlen(" not found") + 1;
char* message = alloca(length);
snprintf(message, length, "%s not found", work->path);
JS_SetPropertyUint32(context, params, 0, JS_NewString(context, message));
JS_SetPropertyStr(context, object, "params", params);
JS_SetPropertyStr(context, object, "id", JS_NewInt32(context, -1));
_http_json_send(work->app->request, context, object);
JS_FreeValue(context, object);
}
else
{
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "action", JS_NewString(context, "session"));
JS_SetPropertyStr(context, object, "credentials", JS_DupValue(context, work->app->credentials));
JS_SetPropertyStr(context, object, "id", JS_NewString(context, work->blob_id));
if (work->identity_info)
{
JSValue identities = JS_NewArray(context);
for (int i = 0; i < work->identity_info->count; i++)
{
JS_SetPropertyUint32(context, identities, i, JS_NewString(context, work->identity_info->identity[i]));
}
JS_SetPropertyStr(context, object, "identities", identities);
JSValue names = JS_NewObject(context);
for (int i = 0; i < work->identity_info->count; i++)
{
JS_SetPropertyStr(context, names, work->identity_info->identity[i],
JS_NewString(context, work->identity_info->name[i] ? work->identity_info->name[i] : work->identity_info->identity[i]));
}
JS_SetPropertyStr(context, object, "names", names);
JS_SetPropertyStr(context, object, "identity", JS_NewString(context, work->identity_info->active_identity));
}
_http_json_send(work->app->request, context, object);
JS_FreeValue(context, object);
JSValue edit_only = JS_GetPropertyStr(context, work->message, "edit_only");
bool is_edit_only = JS_ToBool(context, edit_only) > 0;
JS_FreeValue(context, edit_only);
if (is_edit_only)
{
JSValue global = JS_GetGlobalObject(context);
JSValue version = JS_GetPropertyStr(context, global, "version");
JS_FreeValue(context, global);
JSValue ready = JS_NewObject(context);
JS_SetPropertyStr(context, ready, "action", JS_NewString(context, "ready"));
JS_SetPropertyStr(context, ready, "version", JS_Call(context, version, JS_NULL, 0, NULL));
JS_SetPropertyStr(context, ready, "edit_only", JS_TRUE);
_http_json_send(work->app->request, context, ready);
JS_FreeValue(context, ready);
JS_FreeValue(context, version);
}
else
{
JSValue options = JS_NewObject(context);
JSValue api = JS_GetPropertyStr(context, work->message, "api");
JS_SetPropertyStr(context, options, "api", JS_IsUndefined(api) ? JS_NewArray(context) : api);
JS_SetPropertyStr(context, options, "credentials", JS_DupValue(context, work->app->credentials));
JS_SetPropertyStr(context, options, "packageOwner", work->user_app ? JS_NewString(context, work->user_app->user) : JS_UNDEFINED);
JS_SetPropertyStr(context, options, "packageName", work->user_app ? JS_NewString(context, work->user_app->app) : JS_UNDEFINED);
JS_SetPropertyStr(context, options, "url", JS_GetPropertyStr(context, work->message, "url"));
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue get_process_blob = JS_GetPropertyStr(context, exports, "getProcessBlob");
static int64_t s_session_id;
char session_id[64];
snprintf(session_id, sizeof(session_id), "app_%" PRId64, ++s_session_id);
JSValue args[] = {
JS_NewString(context, work->blob_id),
JS_NewString(context, session_id),
options,
};
JSValue result = JS_Call(context, get_process_blob, JS_UNDEFINED, tf_countof(args), args);
tf_util_report_error(context, result);
JSValue promise_then = JS_GetPropertyStr(context, result, "then");
work->app->opaque = JS_NewObject(context);
JS_SetOpaque(work->app->opaque, work->app);
JSValue then = JS_NewCFunctionData(context, _httpd_app_on_process_start, 0, 0, 1, &work->app->opaque);
JSValue promise = JS_Call(context, promise_then, result, 1, &then);
tf_util_report_error(context, promise);
JS_FreeValue(context, promise);
/* except? */
JS_FreeValue(context, then);
JS_FreeValue(context, promise_then);
JS_FreeValue(context, result);
JS_FreeValue(context, get_process_blob);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
}
}
tf_http_request_unref(work->app->request);
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->path);
JS_FreeValue(context, work->message);
tf_free(work->identity_info);
tf_free(work->user_app);
tf_free(work);
}
static void _httpd_app_message_hello(app_t* work, JSValue message)
{
JSContext* context = work->request->context;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_http_request_ref(work->request);
work->got_hello = true;
JSValue session = JS_IsObject(work->credentials) ? JS_GetPropertyStr(context, work->credentials, "session") : JS_UNDEFINED;
const char* user = tf_util_get_property_as_string(context, session, "name");
JS_FreeValue(context, session);
app_hello_t* hello = tf_malloc(sizeof(app_hello_t));
*hello = (app_hello_t) {
.app = work,
.user = user,
.message = JS_DupValue(context, message),
.path = tf_util_get_property_as_string(context, message, "path"),
};
tf_ssb_run_work(ssb, _httpd_app_hello_work, _httpd_app_hello_after_work, hello);
}
static bool _httpd_app_message_call_client_api(app_t* work, JSValue message, const char* action_string)
{
bool called = false;
JSContext* context = work->request->context;
JSValue client_api = JS_IsObject(work->process) ? JS_GetPropertyStr(context, work->process, "client_api") : JS_UNDEFINED;
JSValue callback = JS_IsObject(client_api) ? JS_GetPropertyStr(context, client_api, action_string) : JS_UNDEFINED;
if (!JS_IsUndefined(callback))
{
JSValue result = JS_Call(context, callback, JS_NULL, 1, &message);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
called = true;
}
JS_FreeValue(context, callback);
JS_FreeValue(context, client_api);
return called;
}
static bool _httpd_app_message_call_message_handler(app_t* work, JSValue message)
{
bool called = false;
JSContext* context = work->request->context;
JSValue event_handlers = JS_GetPropertyStr(context, work->process, "eventHandlers");
JSValue handler_array = JS_GetPropertyStr(context, event_handlers, "message");
if (!JS_IsUndefined(handler_array))
{
for (int i = 0; i < tf_util_get_length(context, handler_array); i++)
{
JSValue handler = JS_GetPropertyUint32(context, handler_array, i);
JSValue result = JS_Call(context, handler, JS_NULL, 1, &message);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, handler);
called = true;
}
}
JS_FreeValue(context, handler_array);
JS_FreeValue(context, event_handlers);
return called;
}
static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const void* data, size_t size)
{
tf_printf("REQUEST MESSAGE %.*s\n", (int)size, (const char*)data);
app_t* work = request->user_data;
JSContext* context = request->context;
tf_task_t* task = tf_task_get(context);
work->last_active_ms = uv_now(tf_task_get_loop(task));
switch (op_code)
{
/* TEXT */
case 0x1:
/* BINARY */
case 0x2:
{
JSValue message = JS_ParseJSON(context, data, size, NULL);
if (JS_IsException(message) || !JS_IsObject(message))
{
tf_util_report_error(context, message);
tf_http_request_websocket_close(request);
}
else
{
JSValue action = JS_GetPropertyStr(context, message, "action");
const char* action_string = JS_ToCString(context, action);
if (action_string && !work->got_hello && strcmp(action_string, "hello") == 0)
{
_httpd_app_message_hello(work, message);
}
else if (!_httpd_app_message_call_client_api(work, message, action_string))
{
_httpd_app_message_call_message_handler(work, message);
}
JS_FreeCString(context, action_string);
JS_FreeValue(context, action);
}
JS_FreeValue(context, message);
}
break;
/* CLOSE */
case 0x8:
if (work->taskstub)
{
JSValue result = tf_taskstub_kill(work->taskstub);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
work->taskstub = NULL;
}
_httpd_app_kill_task(work);
tf_http_request_websocket_send(request, 0x8, data, tf_min(size, sizeof(uint16_t)));
break;
/* PONG */
case 0xa:
@@ -256,17 +614,48 @@ static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const
}
}
static void _httpd_app_on_timer_close(uv_handle_t* handle)
{
app_t* work = handle->data;
handle->data = NULL;
tf_free(work);
}
static void _httpd_app_on_close(tf_http_request_t* request)
{
tf_printf("REQUEST CLOSE\n");
JSContext* context = request->context;
app_t* work = request->user_data;
JS_SetOpaque(work->opaque, NULL);
JS_FreeValue(context, work->credentials);
tf_free(work);
_httpd_app_kill_task(work);
JS_FreeValue(context, work->process);
JS_FreeValue(context, work->opaque);
work->process = JS_UNDEFINED;
uv_close((uv_handle_t*)&work->timer, _httpd_app_on_timer_close);
tf_http_request_unref(request);
}
static void _httpd_app_on_timer(uv_timer_t* timer)
{
app_t* app = timer->data;
uint64_t now_ms = uv_now(timer->loop);
uint64_t repeat_ms = uv_timer_get_repeat(timer);
if (now_ms - app->last_active_ms < repeat_ms)
{
/* Active. */
}
else if (app->last_ping_ms > app->last_active_ms)
{
/* Timed out. */
tf_http_request_websocket_close(app->request);
}
else
{
tf_http_request_websocket_send(app->request, 0x9, NULL, 0);
app->last_ping_ms = now_ms;
}
}
static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_t* work = user_data;
@@ -349,7 +738,8 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
tf_http_request_websocket_upgrade(request);
tf_http_respond(request, 101, headers, headers_count, NULL, 0);
/* What now? */
uv_timer_start(&work->timer, _httpd_app_on_timer, 6 * 1000, 6 * 1000);
tf_free((void*)cookie);
JS_FreeCString(context, name_string);
@@ -360,7 +750,7 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
request->user_data = work;
}
static void _tf_httpd_endpoint_app_socket_c(tf_http_request_t* request)
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
const char* header_connection = tf_http_request_get_header(request, "connection");
const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
@@ -388,69 +778,9 @@ static void _tf_httpd_endpoint_app_socket_c(tf_http_request_t* request)
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
}
static void _tf_httpd_endpoint_app_socket_js(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response,
};
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
static bool checked_env;
static bool use_c;
if (!checked_env)
{
char buffer[8] = { 0 };
size_t buffer_size = sizeof(buffer);
use_c = uv_os_getenv("TF_APP_C", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
checked_env = true;
}
if (use_c)
{
_tf_httpd_endpoint_app_socket_c(request);
}
else
{
_tf_httpd_endpoint_app_socket_js(request);
}
}

View File

@@ -587,7 +587,7 @@ tf_httpd_user_app_t* tf_httpd_parse_user_app_from_path(const char* path, const c
size_t length = strlen(path);
size_t suffix_length = expected_suffix ? strlen(expected_suffix) : 0;
if (length < suffix_length || strcmp(path + length - suffix_length, expected_suffix) != 0)
if (expected_suffix && (length < suffix_length || strcmp(path + length - suffix_length, expected_suffix) != 0))
{
return NULL;
}
@@ -602,6 +602,14 @@ tf_httpd_user_app_t* tf_httpd_parse_user_app_from_path(const char* path, const c
size_t user_length = (size_t)(slash - user);
const char* app = slash + 1;
size_t app_length = (size_t)(length - suffix_length - user_length - 3);
if (!expected_suffix)
{
char* app_slash = strchr(app, '/');
if (app_slash)
{
app_length = tf_min((size_t)(app_slash - app), app_length);
}
}
tf_httpd_user_app_t* result = tf_malloc(sizeof(tf_httpd_user_app_t) + user_length + 1 + app_length + 1);
*result = (tf_httpd_user_app_t) {

View File

@@ -19,7 +19,7 @@
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>27</string>
<string>49</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>LSRequiresIPhoneOS</key>

View File

@@ -2055,6 +2055,13 @@ void tf_run_thread_start(const char* zip_path)
#else
int main(int argc, char* argv[])
{
#if defined(__linux__)
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
{
tf_printf("Unable to set no new privileges flag: %s\n", strerror(errno));
return EXIT_FAILURE;
}
#endif
setvbuf(stdout, NULL, _IONBF, 0);
_startup(argc, argv);
ares_library_init(0);

View File

@@ -1122,8 +1122,7 @@ static bool _tf_ssb_verify_and_strip_signature_internal(
const char* sigstr = JS_ToCString(context, sigval);
const char* sigkind = strstr(str, ".sig.ed25519");
JSValue authorval = JS_GetPropertyStr(context, val, "author");
const char* author = JS_ToCString(context, authorval);
const char* author = tf_util_get_property_as_string(context, val, "author");
const char* author_id = author && *author == '@' ? author + 1 : author;
const char* type = strstr(author_id, ".ed25519");
@@ -1172,7 +1171,6 @@ static bool _tf_ssb_verify_and_strip_signature_internal(
JS_FreeCString(context, sigstr);
JS_FreeCString(context, str);
JS_FreeValue(context, sigval);
JS_FreeValue(context, authorval);
if (verified)
{
JS_FreeValue(context, signature);
@@ -3046,25 +3044,21 @@ static void _tf_ssb_connection_tunnel_callback(
tf_ssb_connection_rpc_send(connection, flags, -request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message_val = JS_GetPropertyStr(context, args, "message");
JSValue stack_val = JS_GetPropertyStr(context, args, "stack");
const char* message_string = tf_util_get_property_as_string(context, args, "message");
const char* stack_string = tf_util_get_property_as_string(context, args, "stack");
char buffer[1024];
if (!JS_IsUndefined(message_val))
if (message_string)
{
const char* message_string = JS_ToCString(context, message_val);
const char* stack_string = JS_ToCString(context, stack_val);
snprintf(buffer, sizeof(buffer), "Tunnel error: %s\n%s", message_string, stack_string);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
}
else
{
snprintf(buffer, sizeof(buffer), "Tunnel error: %.*s", (int)size, message);
}
JS_FreeValue(context, stack_val);
JS_FreeValue(context, message_val);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
tf_ssb_connection_close(tunnel, buffer);
}
@@ -4049,8 +4043,7 @@ void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequ
if (!JS_IsUndefined(message_keys))
{
JSValue message = JS_GetPropertyStr(context, message_keys, "value");
JSValue author = JS_GetPropertyStr(context, message, "author");
const char* author_string = JS_ToCString(context, author);
const char* author_string = tf_util_get_property_as_string(context, message, "author");
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
{
@@ -4068,7 +4061,6 @@ void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequ
}
JS_FreeCString(context, author_string);
JS_FreeValue(context, author);
JS_FreeValue(context, message);
}
}
@@ -4774,6 +4766,7 @@ char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipien
uint8_t* payload = tf_malloc(payload_size);
char* encoded = NULL;
uint8_t* p = payload;
memcpy(p, nonce, sizeof(nonce));
p += sizeof(nonce);
@@ -4788,17 +4781,17 @@ char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipien
tf_ssb_id_str_to_bin(key, recipients[i]);
if (crypto_sign_ed25519_pk_to_curve25519(recipient, key) != 0)
{
return NULL;
goto fail;
}
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, secret_key, recipient) != 0)
{
return NULL;
goto fail;
}
if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0)
{
return NULL;
goto fail;
}
p += crypto_secretbox_MACBYTES + sizeof(length_and_key);
@@ -4806,16 +4799,17 @@ char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipien
if (crypto_secretbox_easy(p, (const uint8_t*)message, message_size, nonce, body_key) != 0)
{
return NULL;
goto fail;
}
p += crypto_secretbox_MACBYTES + message_size;
assert((size_t)(p - payload) == payload_size);
char* encoded = tf_malloc(payload_size * 2 + 5);
encoded = tf_malloc(payload_size * 2 + 5);
size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5);
memcpy(encoded + encoded_length, ".box", 5);
fail:
tf_free(payload);
return encoded;
}

View File

@@ -314,7 +314,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.rowid, "
"old.content); END");
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%ltrim%'"))
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%INSTR%'"))
{
tf_printf("Deleting incorrect messages_refs...\n");
_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS messages_refs");
@@ -337,7 +337,8 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"(j.value LIKE '#%' AND INSTR(j.value, ' ') = 0 AND INSTR(j.value, char(9)) = 0 AND INSTR(j.value, char(10)) = 0 AND INSTR(j.value, char(13)) = 0 AND INSTR(j.value, "
"',') = 0) "
"ON CONFLICT DO NOTHING");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
tf_printf("Done.\n");
@@ -351,7 +352,8 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"(j.value LIKE '#%' AND INSTR(j.value, ' ') = 0 AND INSTR(j.value, char(9)) = 0 AND INSTR(j.value, char(10)) = 0 AND INSTR(j.value, char(13)) = 0 AND INSTR(j.value, ',') "
"= 0) "
"ON CONFLICT DO NOTHING; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_refs");
_tf_ssb_db_exec(db, "CREATE TRIGGER IF NOT EXISTS messages_ad_refs AFTER DELETE ON messages BEGIN DELETE FROM messages_refs WHERE messages_refs.message = old.id; END");
@@ -2555,7 +2557,7 @@ const char* tf_ssb_db_get_global_setting_string_alloc(sqlite3* db, const char* n
return result;
}
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value)
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, const char* value)
{
tf_setting_kind_t kind = tf_util_get_global_setting_kind(name);
if (kind == k_kind_unknown)
@@ -2953,3 +2955,52 @@ void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double t
sqlite3_finalize(statement);
}
}
char* tf_ssb_db_swap_with_server_identity(sqlite3* db, const char* user, const char* user_id, const char* server_id)
{
tf_printf("SWAP user=%s user_id=%s server_id=%s\n", user, user_id, server_id);
char* result = NULL;
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "UPDATE identities SET user = ? WHERE user = ? AND '@' || public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, server_id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1 &&
sqlite3_reset(statement) == SQLITE_OK && sqlite3_bind_text(statement, 1, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, user_id, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1)
{
char* commit_error = NULL;
if (sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, &commit_error) != SQLITE_OK)
{
result = commit_error ? tf_strdup(commit_error) : tf_strdup(sqlite3_errmsg(db));
}
if (commit_error)
{
sqlite3_free(commit_error);
}
}
else
{
result = tf_strdup(sqlite3_errmsg(db) ? sqlite3_errmsg(db) : "swap failed");
}
sqlite3_finalize(statement);
}
else
{
result = tf_strdup(sqlite3_errmsg(db));
}
}
else
{
result = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
return result;
}

View File

@@ -32,7 +32,7 @@ void tf_ssb_db_init_reader(sqlite3* db);
** @param ssb The SSB instance.
** @param id The message identifier.
** @param[out] out_blob Populated with the message content.
** @param[out] out_size POpulated with the size of the message content.
** @param[out] out_size Populated with the size of the message content.
** @return true If the message content was found and retrieved.
*/
bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size);
@@ -454,7 +454,7 @@ const char* tf_ssb_db_resolve_index(sqlite3* db, const char* host);
/**
** Verify an author's feed.
** @param ssb The SSB instance.
** @param id The author'd identity.
** @param id The author's identity.
** @param debug_sequence Message sequence number to debug if non-zero.
** @param fix Fix invalid messages when possible.
** @return true If the feed verified successfully.
@@ -511,10 +511,10 @@ const char* tf_ssb_db_get_global_setting_string_alloc(sqlite3* db, const char* n
** Set a global setting from a string representation of its value.
** @param db The database.
** @param name The setting name.
** @param value The settinv value.
** @param value The setting value.
** @return true if the setting was set.
*/
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, char* value);
bool tf_ssb_db_set_global_setting_from_string(sqlite3* db, const char* name, const char* value);
/**
** Get the latest profile information for the given identity.
@@ -647,4 +647,14 @@ bool tf_ssb_db_is_blocked(sqlite3* db, const char* id);
*/
void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double timestamp, void* user_data), void* user_data);
/**
** Swap a user's identity with the server identity.
** @param db The database.
** @param user The user.
** @param user_id The user identity.
** @param server_id The server identity.
** @return Null on success or an error message on error. Free with tf_free().
*/
char* tf_ssb_db_swap_with_server_identity(sqlite3* db, const char* user, const char* user_id, const char* server_id);
/** @} */

View File

@@ -7,18 +7,11 @@
#include "ssb.h"
#include "util.js.h"
#include "sodium/crypto_box.h"
#include "sodium/crypto_scalarmult.h"
#include "sodium/crypto_scalarmult_curve25519.h"
#include "sodium/crypto_scalarmult_ed25519.h"
#include "sodium/crypto_secretbox.h"
#include "sodium/crypto_sign.h"
#include "sodium/randombytes.h"
#include "sqlite3.h"
#include "string.h"
#include "uv.h"
#include <assert.h>
#include <inttypes.h>
static const int k_sql_async_timeout_ms = 60 * 1000;
@@ -252,117 +245,6 @@ static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val,
return result;
}
typedef struct _swap_with_server_identity_t
{
char server_id[k_id_base64_len];
char id[k_id_base64_len];
JSValue promise[2];
char* error;
char user[];
} swap_with_server_identity_t;
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
swap_with_server_identity_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (tf_ssb_db_user_has_permission(ssb, db, work->user, "administration"))
{
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "UPDATE identities SET user = ? WHERE user = ? AND '@' || public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->server_id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1 &&
sqlite3_reset(statement) == SQLITE_OK && sqlite3_bind_text(statement, 1, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->id, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1)
{
char* commit_error = NULL;
if (sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, &commit_error) != SQLITE_OK)
{
work->error = commit_error ? tf_strdup(commit_error) : tf_strdup(sqlite3_errmsg(db));
}
if (commit_error)
{
sqlite3_free(commit_error);
}
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db) ? sqlite3_errmsg(db) : "swap failed");
}
sqlite3_finalize(statement);
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db));
}
}
else
{
work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
}
else
{
work->error = tf_strdup("not administrator");
}
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
swap_with_server_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
JSValue exception = JS_GetException(context);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
tf_free(work->error);
JS_FreeValue(context, exception);
JS_FreeValue(context, arg);
}
else
{
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_swap_with_server_identity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
*work = (swap_with_server_identity_t) { 0 };
tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
tf_string_set(work->id, sizeof(work->id), id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
}
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
@@ -1656,303 +1538,6 @@ static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, i
return result ? JS_TRUE : JS_FALSE;
}
static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (!user || !identity)
{
tf_printf("user=%p identity=%p out_private_key=%p\n", user, identity, out_private_key);
return false;
}
bool success = false;
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "SELECT private_key FROM identities WHERE user = ? AND public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, *identity == '@' ? identity + 1 : identity, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
uint8_t key[crypto_sign_SECRETKEYBYTES] = { 0 };
int length = tf_base64_decode((const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0) - strlen(".ed25519"), key, sizeof(key));
if (length == crypto_sign_SECRETKEYBYTES)
{
success = crypto_sign_ed25519_sk_to_curve25519(out_private_key, key) == 0;
}
}
}
sqlite3_finalize(statement);
}
return success;
}
static bool _tf_ssb_get_private_key_curve25519(tf_ssb_t* ssb, sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (_tf_ssb_get_private_key_curve25519_internal(db, user, identity, out_private_key))
{
return true;
}
if (tf_ssb_db_user_has_permission(ssb, db, user, "administration"))
{
return _tf_ssb_get_private_key_curve25519_internal(db, ":admin", identity, out_private_key);
}
return false;
}
typedef struct _private_message_encrypt_t
{
const char* signer_user;
const char* signer_identity;
const char* recipients[k_max_private_message_recipients];
int recipient_count;
const char* message;
size_t message_size;
JSValue promise[2];
bool error_id_not_found;
char* encrypted;
size_t encrypted_length;
} private_message_encrypt_t;
static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_encrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->signer_user, work->signer_identity, private_key);
tf_ssb_release_db_reader(ssb, db);
if (found)
{
work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size);
work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0;
}
else
{
work->error_id_not_found = true;
}
}
static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
private_message_encrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (!work->encrypted)
{
result = JS_ThrowInternalError(context, "Encrypt failed.");
}
else if (work->error_id_not_found)
{
result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", work->signer_identity, work->signer_user);
}
else
{
result = JS_NewStringLen(context, work->encrypted, work->encrypted_length);
tf_free((void*)work->encrypted);
}
for (int i = 0; i < work->recipient_count; i++)
{
tf_free((void*)work->recipients[i]);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->signer_user);
JS_FreeCString(context, work->signer_identity);
JS_FreeCString(context, work->message);
tf_free(work);
}
static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
int recipient_count = tf_util_get_length(context, argv[2]);
if (recipient_count < 1 || recipient_count > k_max_private_message_recipients)
{
return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients);
}
char* recipients[k_max_private_message_recipients] = { 0 };
for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
{
JSValue recipient = JS_GetPropertyUint32(context, argv[2], i);
const char* id = JS_ToCString(context, recipient);
if (id)
{
recipients[i] = tf_strdup(id);
JS_FreeCString(context, id);
}
JS_FreeValue(context, recipient);
}
if (JS_IsUndefined(result))
{
const char* signer_user = JS_ToCString(context, argv[0]);
const char* signer_identity = JS_ToCString(context, argv[1]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[3]);
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
private_message_encrypt_t* work = tf_malloc(sizeof(private_message_encrypt_t));
*work = (private_message_encrypt_t) {
.signer_user = signer_user,
.signer_identity = signer_identity,
.recipient_count = recipient_count,
.message = message,
.message_size = message_size,
};
static_assert(sizeof(work->recipients) == sizeof(recipients), "size mismatch");
memcpy(work->recipients, recipients, sizeof(recipients));
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_private_message_encrypt_work, _tf_ssb_private_message_encrypt_after_work, work);
}
return result;
}
typedef struct _private_message_decrypt_t
{
const char* user;
const char* identity;
size_t message_size;
const char* message;
const char* decrypted;
size_t decrypted_size;
const char* error;
JSValue promise[2];
} private_message_decrypt_t;
static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_decrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->user, work->identity, private_key);
tf_ssb_release_db_reader(ssb, db);
if (found)
{
if (work->message_size >= strlen(".box") && memcmp(work->message + work->message_size - strlen(".box"), ".box", strlen(".box")) == 0)
{
uint8_t* decoded = tf_malloc(work->message_size);
int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size);
uint8_t* nonce = decoded;
uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
{
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
{
enum
{
k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
};
for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes;
p += k_recipient_header_bytes)
{
uint8_t out[k_recipient_header_bytes] = { 0 };
int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
if (opened != -1)
{
int recipients = (int)out[0];
uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
size_t body_size = decoded + decoded_length - body;
uint8_t* decrypted = tf_malloc(body_size);
uint8_t* key = out + 1;
if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
{
work->decrypted = (const char*)decrypted;
work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
}
else
{
work->error = "Received key to open secret box containing message body, but it did not work.";
}
}
}
}
else
{
work->error = "crypto_scalarmult failed.";
}
}
else
{
work->error = "Encrypted message was not long enough to contain its one-time public key.";
}
tf_free(decoded);
}
else
{
work->error = "Message does not end in \".box\".";
}
}
else
{
work->error = "Private key not found for user.";
}
}
static void _tf_ssb_private_message_decrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
private_message_decrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue result = JS_ThrowInternalError(context, "%s", work->error);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else if (work->decrypted)
{
JSValue result = JS_NewStringLen(context, work->decrypted, work->decrypted_size);
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else
{
JSValue result = JS_UNDEFINED;
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->identity);
JS_FreeCString(context, work->message);
tf_free((void*)work->decrypted);
tf_free(work);
}
static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* user = JS_ToCString(context, argv[0]);
const char* identity = JS_ToCString(context, argv[1]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[2]);
private_message_decrypt_t* work = tf_malloc(sizeof(private_message_decrypt_t));
*work = (private_message_decrypt_t) {
.user = user,
.identity = identity,
.message_size = message_size,
.message = message,
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_private_message_decrypt_work, _tf_ssb_private_message_decrypt_after_work, work);
return result;
}
typedef struct _following_t
{
JSContext* context;
@@ -2172,129 +1757,6 @@ static JSValue _tf_ssb_port(JSContext* context, JSValueConst this_val, int argc,
return JS_NewInt32(context, tf_ssb_server_get_port(ssb));
}
typedef struct _modify_block_t
{
char id[k_id_base64_len];
bool add;
JSValue promise[2];
} modify_block_t;
static void _tf_ssb_modify_block_work(tf_ssb_t* ssb, void* user_data)
{
modify_block_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (work->add)
{
tf_ssb_db_add_block(db, work->id);
}
else
{
tf_ssb_db_remove_block(db, work->id);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_modify_block_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
modify_block_t* request = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_Call(context, request->promise[0], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
tf_free(request);
}
static JSValue _tf_ssb_add_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) { .add = true };
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
return result;
}
static JSValue _tf_ssb_remove_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) { .add = false };
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
return result;
}
typedef struct _block_t
{
char id[k_id_base64_len];
double timestamp;
} block_t;
typedef struct _get_blocks_t
{
block_t* blocks;
int count;
JSValue promise[2];
} get_blocks_t;
static void _get_blocks_callback(const char* id, double timestamp, void* user_data)
{
get_blocks_t* work = user_data;
work->blocks = tf_resize_vec(work->blocks, sizeof(block_t) * (work->count + 1));
work->blocks[work->count] = (block_t) { .timestamp = timestamp };
tf_string_set(work->blocks[work->count].id, sizeof(work->blocks[work->count].id), id);
work->count++;
}
static void _tf_ssb_get_blocks_work(tf_ssb_t* ssb, void* user_data)
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_blocks(db, _get_blocks_callback, user_data);
tf_ssb_release_db_reader(ssb, db);
}
static void _tf_ssb_get_blocks_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_blocks_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JSValue entry = JS_NewObject(context);
JS_SetPropertyStr(context, entry, "id", JS_NewString(context, work->blocks[i].id));
JS_SetPropertyStr(context, entry, "timestamp", JS_NewFloat64(context, work->blocks[i].timestamp));
JS_SetPropertyUint32(context, result, i, entry);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->blocks);
tf_free(work);
}
static JSValue _tf_ssb_get_blocks(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
get_blocks_t* work = tf_malloc(sizeof(get_blocks_t));
*work = (get_blocks_t) { 0 };
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_blocks_work, _tf_ssb_get_blocks_after_work, work);
return result;
}
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
{
JS_NewClassID(&_tf_ssb_classId);
@@ -2319,10 +1781,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
JS_SetPropertyStr(context, object, "privateMessageEncrypt", JS_NewCFunction(context, _tf_ssb_private_message_encrypt, "privateMessageEncrypt", 4));
JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));
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));
@@ -2350,9 +1809,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object_internal, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object_internal, "addEventListener", JS_NewCFunction(context, _tf_ssb_add_event_listener, "addEventListener", 2));
JS_SetPropertyStr(context, object_internal, "removeEventListener", JS_NewCFunction(context, _tf_ssb_remove_event_listener, "removeEventListener", 2));
JS_SetPropertyStr(context, object_internal, "addBlock", JS_NewCFunction(context, _tf_ssb_add_block, "addBlock", 1));
JS_SetPropertyStr(context, object_internal, "removeBlock", JS_NewCFunction(context, _tf_ssb_remove_block, "removeBlock", 1));
JS_SetPropertyStr(context, object_internal, "getBlocks", JS_NewCFunction(context, _tf_ssb_get_blocks, "getBlocks", 0));
JS_FreeValue(context, global);
}

View File

@@ -125,9 +125,7 @@ static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags
}
else
{
JSValue key = JS_GetPropertyStr(context, arg, "key");
id = JS_ToCString(context, key);
JS_FreeValue(context, key);
id = tf_util_get_property_as_string(context, arg, "key");
}
blobs_get_work_t* work = tf_malloc(sizeof(blobs_get_work_t));
@@ -313,25 +311,20 @@ static void _tf_ssb_rpc_tunnel_callback(tf_ssb_connection_t* connection, uint8_t
tf_ssb_connection_remove_request(connection, request_number);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message_val = JS_GetPropertyStr(context, args, "message");
JSValue stack_val = JS_GetPropertyStr(context, args, "stack");
const char* message_string = tf_util_get_property_as_string(context, args, "message");
const char* stack_string = tf_util_get_property_as_string(context, args, "stack");
char buffer[1024];
if (!JS_IsUndefined(message_val))
if (message_string)
{
const char* message_string = JS_ToCString(context, message_val);
const char* stack_string = JS_ToCString(context, stack_val);
snprintf(buffer, sizeof(buffer), "Error from tunnel: %s\n%s", message_string, stack_string);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
}
else
{
snprintf(buffer, sizeof(buffer), "Error from tunnel: %.*s", (int)size, message);
}
JS_FreeValue(context, stack_val);
JS_FreeValue(context, message_val);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
}
else
{
@@ -762,8 +755,7 @@ static void _tf_ssb_rpc_connection_room_attendants_callback(
{
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue type = JS_GetPropertyStr(context, args, "type");
const char* type_string = JS_ToCString(context, type);
const char* type_string = tf_util_get_property_as_string(context, args, "type");
if (!type_string)
{
tf_ssb_connection_rpc_send_error(connection, flags, -request_number, "Missing type.");
@@ -788,25 +780,21 @@ static void _tf_ssb_rpc_connection_room_attendants_callback(
}
else if (strcmp(type_string, "joined") == 0)
{
JSValue id = JS_GetPropertyStr(context, args, "id");
const char* id_string = JS_ToCString(context, id);
const char* id_string = tf_util_get_property_as_string(context, args, "id");
if (id_string)
{
tf_ssb_connection_add_room_attendant(connection, id_string);
}
JS_FreeCString(context, id_string);
JS_FreeValue(context, id);
}
else if (strcmp(type_string, "left") == 0)
{
JSValue id = JS_GetPropertyStr(context, args, "id");
const char* id_string = JS_ToCString(context, id);
const char* id_string = tf_util_get_property_as_string(context, args, "id");
if (id_string)
{
tf_ssb_connection_remove_room_attendant(connection, id_string);
}
JS_FreeCString(context, id_string);
JS_FreeValue(context, id);
}
else
{
@@ -815,20 +803,17 @@ static void _tf_ssb_rpc_connection_room_attendants_callback(
tf_ssb_connection_rpc_send_error(connection, flags, -request_number, buffer);
}
JS_FreeCString(context, type_string);
JS_FreeValue(context, type);
}
static bool _is_error(JSContext* context, JSValue message)
{
JSValue name = JS_GetPropertyStr(context, message, "name");
const char* name_string = JS_ToCString(context, name);
const char* name_string = tf_util_get_property_as_string(context, message, "name");
bool is_error = false;
if (name_string && strcmp(name_string, "Error") == 0)
{
is_error = true;
}
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
return is_error;
}
@@ -1937,12 +1922,10 @@ static void _tf_ssb_rpc_invite_use(tf_ssb_connection_t* connection, uint8_t flag
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue array = JS_GetPropertyStr(context, args, "args");
JSValue object = JS_GetPropertyUint32(context, array, 0);
JSValue feed = JS_GetPropertyStr(context, object, "feed");
tf_ssb_connection_get_id(connection, work->invite_public_key, sizeof(work->invite_public_key));
const char* id = JS_ToCString(context, feed);
const char* id = tf_util_get_property_as_string(context, object, "feed");
tf_string_set(work->id, sizeof(work->id), id);
JS_FreeCString(context, id);
JS_FreeValue(context, feed);
JS_FreeValue(context, object);
JS_FreeValue(context, array);
tf_ssb_connection_run_work(connection, _tf_ssb_rpc_invite_use_work, _tf_ssb_rpc_invite_use_after_work, work);

View File

@@ -922,45 +922,8 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options)
}
#if !TARGET_OS_IPHONE
static void _write_file(const char* path, const char* contents)
{
FILE* file = fopen(path, "w");
if (!file)
{
tf_printf("Unable to write %s: %s.\n", path, strerror(errno));
abort();
}
fputs(contents, file);
fclose(file);
}
#define TEST_ARGS " --args=ssb_port=0,http_port=0"
void tf_ssb_test_encrypt(const tf_test_options_t* options)
{
_write_file("out/test.js",
"async function main() {\n"
" let a = await ssb.createIdentity('test');\n"
" let b = await ssb.createIdentity('test');\n"
" let c = await ssb.privateMessageEncrypt('test', a, [a, b], \"{'foo': 1}\");\n"
" if (!c.endsWith('.box')) {\n"
" exit(1);\n"
" }\n"
" print(await ssb.privateMessageDecrypt('test', a, c));\n"
"}\n"
"main().catch(() => exit(2));\n");
unlink("out/testdb.sqlite");
char command[256];
snprintf(command, sizeof(command), "%s run --db-path=out/testdb.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command);
int result = system(command);
(void)result;
assert(WIFEXITED(result));
tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
}
static void _count_broadcasts_callback(
const char* host, const struct sockaddr_in* addr, tf_ssb_broadcast_origin_t origin, tf_ssb_connection_t* tunnel, const uint8_t* pub, void* user_data)
{

View File

@@ -47,12 +47,6 @@ void tf_ssb_test_bench(const tf_test_options_t* options);
*/
void tf_ssb_test_go_ssb_room(const tf_test_options_t* options);
/**
** Test encrypting a private message.
** @param options The test options.
*/
void tf_ssb_test_encrypt(const tf_test_options_t* options);
/**
** Test peer exchange.
** @param options The test options.

View File

@@ -102,14 +102,13 @@ static void _tf_taskstub_on_exit(tf_taskstub_t* stub, int64_t status, int termin
tf_packetstream_destroy(stub->_stream);
stub->_stream = NULL;
}
tf_task_remove_child(stub->_owner, stub);
if (stub->_process.data)
{
uv_close((uv_handle_t*)&stub->_process, _taskstub_on_handle_close);
}
else
{
_taskstub_cleanup(stub);
tf_task_remove_child(stub->_owner, stub);
}
}
@@ -205,9 +204,9 @@ static JSValue _taskstub_create(JSContext* context, JSValueConst this_val, int a
}
else
{
/* XXX: This is a leak. */
uv_thread_t* thread = tf_malloc(sizeof(uv_thread_t));
uv_thread_create(thread, _tf_taskstub_run_sandbox_thread, (void*)(intptr_t)fds[1]);
uv_thread_t thread = 0;
uv_thread_create(&thread, _tf_taskstub_run_sandbox_thread, (void*)(intptr_t)fds[1]);
uv_thread_detach(&thread);
}
tf_packetstream_set_on_receive(stub->_stream, tf_task_on_receive_packet, stub);

View File

@@ -934,20 +934,34 @@ static void _tf_test_run(const tf_test_options_t* options, const char* name, voi
tf_free(dup);
}
if ((!opt_in && !options->tests) || specified)
{
#define GREEN "\e[1;32m"
#define GRAY "\e[1;90m"
#define MAGENTA "\e[1;35m"
#define CYAN "\e[1;36m"
#define RESET "\e[0m"
size_t length = strlen("TF_TEST_") + strlen(name) + 1;
char* env_name = alloca(length);
snprintf(env_name, length, "TF_TEST_%s", name);
char buffer[8] = { 0 };
size_t buffer_size = sizeof(buffer);
bool exclude = uv_os_getenv(env_name, buffer, &buffer_size) == 0 && strcmp(buffer, "0") == 0;
if (exclude)
{
tf_printf("Test " GRAY "%s" RESET " disabled by %s.\n", name, env_name);
}
if (((!opt_in && !options->tests) || specified) && !exclude)
{
tf_printf(CYAN "== running test " MAGENTA "%s" CYAN " ==" RESET "\n", name);
test(options);
tf_printf("[" GREEN "pass" RESET "] %s\n", name);
}
#undef GREEN
#undef GRAY
#undef MAGENTA
#undef CYAN
#undef RESET
}
}
#endif
@@ -983,7 +997,6 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "rooms", tf_ssb_test_rooms, false);
_tf_test_run(options, "bench", tf_ssb_test_bench, false);
_tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true);
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
_tf_test_run(options, "publish", tf_ssb_test_publish, false);
_tf_test_run(options, "replicate", tf_ssb_test_replicate, false);

View File

@@ -227,14 +227,12 @@ bool tf_util_report_error(JSContext* context, JSValue value)
tf_printf("ERROR: %s\n", string);
JS_FreeCString(context, string);
JSValue stack = JS_GetPropertyStr(context, value, "stack");
if (!JS_IsUndefined(stack))
const char* stack = tf_util_get_property_as_string(context, value, "stack");
if (stack && *stack)
{
const char* stack_str = JS_ToCString(context, stack);
tf_printf("%s\n", stack_str);
JS_FreeCString(context, stack_str);
tf_printf("%s\n", stack);
}
JS_FreeValue(context, stack);
JS_FreeCString(context, stack);
tf_task_send_error_to_parent(task, value);
is_error = true;
@@ -502,6 +500,24 @@ int tf_util_get_length(JSContext* context, JSValue value)
return result;
}
const char* tf_util_get_property_as_string(JSContext* context, JSValue object, const char* key)
{
if (!JS_IsObject(object))
{
return NULL;
}
JSValue value = JS_GetPropertyStr(context, object, key);
if (JS_IsUndefined(value))
{
return NULL;
}
const char* string = JS_ToCString(context, value);
JS_FreeValue(context, value);
return string;
}
int tf_util_insert_index(const void* key, const void* base, size_t count, size_t size, int (*compare)(const void*, const void*))
{
int lower = 0;

View File

@@ -71,6 +71,15 @@ bool tf_util_report_error(JSContext* context, JSValue value);
*/
int tf_util_get_length(JSContext* context, JSValue value);
/**
** Get an object property by string key as a C-style string.
** @param context The JS context.
** @param object The object.
** @param key The property key.
** @return A string to be freed with JS_FreeCString() or NULL.
*/
const char* tf_util_get_property_as_string(JSContext* context, JSValue object, const char* key);
/**
** Get the index at which to insert into an array in order to preserve sorted order.
** @param key The key being inserted.

View File

@@ -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',))