Compare commits
56 Commits
80c0394ec0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c0ed9fda01 | |||
| 95d263e139 | |||
| 782013f3a3 | |||
| a5ed64f866 | |||
| 88e3494dcf | |||
| abe16dcf66 | |||
| 03a32ca371 | |||
| 09a4fae432 | |||
| 5c173b2695 | |||
| 71493aac51 | |||
| 8fb1850044 | |||
| bbfcbfcae6 | |||
| cd2903c0df | |||
| d873d99b23 | |||
| 1a5392d942 | |||
| ef80c0910c | |||
| 6c641acdd3 | |||
| f0babc6f95 | |||
| 1382eac7e5 | |||
| 79b7252a27 | |||
| 2e8402d11d | |||
| c34065795c | |||
| 1463c18c12 | |||
| f39b0977b7 | |||
| 8f9824e9b7 | |||
| 33392e7c55 | |||
| b4c014fd27 | |||
| 81353b4da9 | |||
| d67297c35b | |||
| 192e9e0955 | |||
| 2449202b5d | |||
| f1876a34ec | |||
| 3c6eeb9cd3 | |||
| c29ab66073 | |||
| d7782d53a1 | |||
| ce3a8c53c6 | |||
| 0af54edac1 | |||
| 2086075f7b | |||
| 14955fa421 | |||
| 1e1059489b | |||
| 68dc5129c8 | |||
| 690b027c0c | |||
| 2f0c379a69 | |||
| 7c1931f529 | |||
| 69f9646955 | |||
| c4ff00dec1 | |||
| 9ce08f79fb | |||
| 0fa9c90ab9 | |||
| 2b191a5345 | |||
| 6381ba6785 | |||
| d84b06f814 | |||
| 3a3b889196 | |||
| 78474e0bea | |||
| 759d5849ba | |||
| 0df9796fb8 | |||
| 95483b3e55 |
@@ -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:
|
||||
|
||||
1
Doxyfile
1
Doxyfile
@@ -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 \
|
||||
|
||||
@@ -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
5
apps/bookclub.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📚",
|
||||
"previous": "&yLHlvKirJEqrekP5lf5BydvzIo/vN+z7K2ACQacxJXE=.sha256"
|
||||
}
|
||||
181
apps/bookclub/app.js
Normal file
181
apps/bookclub/app.js
Normal 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
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
6
apps/bookclub/index.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
${BODY}
|
||||
</body>
|
||||
</html>
|
||||
251
apps/bookclub/w3.css
Normal file
251
apps/bookclub/w3.css
Normal 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}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💡",
|
||||
"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
|
||||
"previous": "&FGkkfFLaEID3V4lUjPbgCOwgEvNXkcVkzs0zzwD/gQ8=.sha256"
|
||||
}
|
||||
|
||||
@@ -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')];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🦀",
|
||||
"previous": "&Do4vIjdE5vJgJ+fIZ10zOeDQcqNd+VUacQl2wzRjGhw=.sha256"
|
||||
"previous": "&eqeAxU0q6n0RZDSd68j44hQ4UtssESqgohsCXN/otwY=.sha256"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()}☰
|
||||
</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()}☰
|
||||
</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}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
252
core/app.js
252
core/app.js
@@ -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, {});
|
||||
};
|
||||
|
||||
/** @} */
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
174
core/core.js
174
core/core.js
@@ -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
2
deps/c-ares
vendored
Submodule deps/c-ares updated: d3a507e920...3ac47ee46e
2
deps/codemirror/cm6.js
vendored
2
deps/codemirror/cm6.js
vendored
File diff suppressed because one or more lines are too long
21
deps/codemirror_src/editor.mjs
vendored
21
deps/codemirror_src/editor.mjs
vendored
@@ -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
206
deps/codemirror_src/package-lock.json
generated
vendored
@@ -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
4
docs/app_development.md
Normal file
@@ -0,0 +1,4 @@
|
||||
@page app_development App Development
|
||||
|
||||
- @subpage app_development_cheat_sheet
|
||||
- @subpage app_development_guide
|
||||
@@ -1,4 +1,4 @@
|
||||
# App Development Cheat Sheet
|
||||
@page app_development_cheat_sheet App Development Cheat Sheet
|
||||
|
||||
Making apps for the impatient tilde friend.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
5
docs/howto.md
Normal file
@@ -0,0 +1,5 @@
|
||||
@page howto How To
|
||||
|
||||
- @subpage upgrading
|
||||
- @subpage transfer_account
|
||||
- @subpage connecting_manyverse
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
5
docs/overview.md
Normal file
@@ -0,0 +1,5 @@
|
||||
@page overview Overview
|
||||
|
||||
- @subpage inspiration
|
||||
- @subpage model
|
||||
- @subpage vision
|
||||
@@ -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
18
docs/transfer_account.md
Normal 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Upgrading
|
||||
@page upgrading Upgrading
|
||||
|
||||
Tilde Friends can be upgraded simply by running a new executable against an
|
||||
existing database.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Vision
|
||||
@page vision Vision
|
||||
|
||||
Tilde Friends is a tool for making and sharing.
|
||||
|
||||
|
||||
6
metadata/en-US/changelogs/49.txt
Normal file
6
metadata/en-US/changelogs/49.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
* Crash fixes.
|
||||
* Fix channels with hyphens and various other characters not working correctly.
|
||||
* Navigation bar and search UI improvements.
|
||||
* Faster loads, though the first launch may be particularly slow as indexes are rebuilt.
|
||||
* Fixed various broken links.
|
||||
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1.
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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"
|
||||
|
||||
832
src/api.js.c
832
src/api.js.c
@@ -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);
|
||||
|
||||
22
src/http.c
22
src/http.c
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
484
src/httpd.app.c
484
src/httpd.app.c
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
34
src/ssb.c
34
src/ssb.c
@@ -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;
|
||||
}
|
||||
|
||||
59
src/ssb.db.c
59
src/ssb.db.c
@@ -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;
|
||||
}
|
||||
|
||||
18
src/ssb.db.h
18
src/ssb.db.h
@@ -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);
|
||||
|
||||
/** @} */
|
||||
|
||||
544
src/ssb.js.c
544
src/ssb.js.c
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
21
src/tests.c
21
src/tests.c
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -129,6 +129,11 @@ try:
|
||||
|
||||
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
|
||||
|
||||
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '.tf-profile', 'shadow_root', '#open_private_chat'], ('click',))
|
||||
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'This is a private message.'))
|
||||
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
|
||||
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
|
||||
|
||||
driver.get('http://localhost:8888/~testuser/test/')
|
||||
select(driver, ['#document'])
|
||||
select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))
|
||||
|
||||
Reference in New Issue
Block a user