30 Commits

Author SHA1 Message Date
d873d99b23 ssb: Handful of URL encoding issues.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m9s
2025-12-15 20:42:57 -05:00
1a5392d942 ssb: Avoid an unnecessary messages load.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m54s
2025-12-15 12:30:27 -05:00
ef80c0910c intro: Scroll to top when switching pages.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 15m36s
2025-12-13 09:01:17 -05:00
6c641acdd3 ssb: Put the hamburger menu on the same line as the welcome text. 2025-12-13 08:57:06 -05:00
f0babc6f95 core: Fix a recently introduced use after free.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 11m44s
2025-12-12 18:24:23 -05:00
1382eac7e5 ssb: Paranoia around trying to avoid showing stale/irrelevant messages. Need to rethink this approach entirely sometime.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m27s
Build Tilde Friends / Build-All (push) Successful in 9m55s
2025-12-11 22:05:06 -05:00
79b7252a27 ssb: Be much more generous about what's allowed in a hashtag ref. Fixes #dev-diary not behaving correctly as a channel.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-11 22:01:38 -05:00
2e8402d11d update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 11m8s
2025-12-11 12:48:44 -05:00
c34065795c build: Get iOS and Android on the same versionCode/CFBundleVersion.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m2s
2025-12-10 12:34:57 -05:00
1463c18c12 core: Unused.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m29s
2025-12-10 12:28:44 -05:00
f39b0977b7 build: Fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m1s
2025-12-09 21:33:03 -05:00
8f9824e9b7 core: Minor simplification around getting account name.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 3m24s
2025-12-09 20:30:09 -05:00
33392e7c55 core: Move ssb.swapWithServerIdentity() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 20:21:02 -05:00
b4c014fd27 core: Remove some ancient unused resizeMe, setHash, and storeBlob message handlers.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-09 19:05:07 -05:00
81353b4da9 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m8s
2025-12-09 18:48:08 -05:00
d67297c35b ssb: Better search feedback. 2025-12-09 18:43:52 -05:00
192e9e0955 core: Better error handling for deleting users.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m36s
Build Tilde Friends / Build-All (push) Successful in 10m5s
2025-12-09 18:10:47 -05:00
2449202b5d core: Move core.deleteUser() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 18:02:47 -05:00
f1876a34ec core: Move core.globalSettingSet to C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-09 13:02:25 -05:00
3c6eeb9cd3 core: Move invoking the permission test to C, at least for adding/removing blocks.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m56s
Build Tilde Friends / Build-All (push) Successful in 10m15s
2025-12-08 21:51:10 -05:00
c29ab66073 update: c-ares 1.34.6.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Successful in 10m19s
2025-12-08 12:17:50 -05:00
d7782d53a1 build: Nope, guess we needed those deps.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 9m46s
2025-12-07 14:41:02 -05:00
ce3a8c53c6 build: Build docs separately and on a later image. Also remove some build dependencies I don't think we need.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 8m9s
2025-12-07 14:22:09 -05:00
0af54edac1 docs: Add some slight organization.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-07 08:42:34 -05:00
2086075f7b core: Minor cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-07 08:25:55 -05:00
14955fa421 build: #buildfix.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-06 23:14:23 -05:00
1e1059489b test: Disable -t=auto on CI.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m17s
2025-12-06 23:01:07 -05:00
68dc5129c8 build: Let's see what happens if CI tries to run tests.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m23s
2025-12-06 22:43:38 -05:00
690b027c0c core: Remove app.js.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-12-06 20:55:02 -05:00
2f0c379a69 core: Implement websocket timeout in C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m6s
2025-12-06 19:13:06 -05:00
39 changed files with 802 additions and 764 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -707,9 +707,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 +756,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 +766,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;
@@ -832,7 +840,7 @@ class TfElement extends LitElement {
: undefined
}
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>🔍<span class="w3-hide-small">Search</span></button>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown}></input>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown} value=${this.search_text()}></input>
</div>
`;
let contents = this.guest

View File

@@ -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,18 @@ 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(this._messages_key, this.make_messages_key());
console.log(
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_hash != this.hash} following=${this._messages_following !== JSON.stringify(this.following)}, channels=${this._channels_latest !== JSON.stringify(Object.keys(this.channels_latest))}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
);
this.load_messages();
}

View File

@@ -428,18 +428,18 @@ class TfTabNewsElement extends LitElement {
</p>
<div>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</div>
<span
style="display: inline-block; width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
style="width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
>
<button
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</button>
Welcome,
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</span>
</div>
${edit_profile}
</div>
<div>

View File

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

View File

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

View File

@@ -1,188 +0,0 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js';
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** 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 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;
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;
}
}
response.send(
JSON.stringify(
Object.assign(
{
action: 'session',
credentials: credentials,
id: blobId,
},
await ssb_internal.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName
)
)
),
0x1
);
if (blobId) {
if (message.edit_only) {
response.send(
JSON.stringify({
action: 'ready',
version: version(),
edit_only: true,
}),
0x1
);
} else {
let sessionId = 'session_' + (g_session_index++).toString();
let options = {
api: message.api || [],
credentials: credentials,
packageOwner: packageOwner,
packageName: packageName,
url: message.url,
};
process = await core.getProcessBlob(blobId, sessionId, options);
}
}
if (process) {
process.client_api.tfrpc = function (message) {
if (message.id) {
let calls = process?.app?.calls;
if (calls) {
let call = calls[message.id];
if (call) {
if (message.error !== undefined) {
call.reject(message.error);
} else {
call.resolve(message.result);
}
delete calls[message.id];
}
}
}
};
process.app._on_output = (message) =>
response.send(JSON.stringify(message), 0x1);
process.app.send();
}
let ping = function () {
let now = Date.now();
let again = true;
if (now - process.lastActive < process.timeout) {
// Active.
} else if (process.lastPing > process.lastActive) {
// We lost them.
if (process.task) {
process.task.kill();
}
again = false;
} else {
// Idle. Ping them.
response.send('', 0x9);
process.lastPing = now;
}
if (again && process.timeout) {
setTimeout(ping, process.timeout);
}
};
if (process && process.timeout > 0) {
setTimeout(ping, process.timeout);
}
} else {
if (process) {
if (process.client_api[message.action]) {
process.client_api[message.action](message);
} else if (process.eventHandlers['message']) {
await core.invoke(process.eventHandlers['message'], [message]);
}
}
}
} else if (event.opCode == 0x8) {
// Close.
if (process && process.task) {
process.task.kill();
}
response.send(event.data, 0x8);
} else if (event.opCode == 0xa) {
// PONG
}
if (process) {
process.lastActive = Date.now();
}
};
response.upgrade(100, {});
};
/** @} */

View File

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

View File

@@ -5,12 +5,6 @@
* @{
*/
/** \cond */
import * as app_module 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;
/**
** App constructor.
@@ -225,7 +217,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;
@@ -243,9 +235,6 @@ async function getProcessBlob(blobId, key, options) {
if (!options?.script || options?.script === 'app.js') {
process.app = new App();
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = k_ping_interval;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@@ -400,34 +389,6 @@ 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) {
@@ -552,30 +513,6 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
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 &&
@@ -650,6 +587,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);
@@ -698,16 +636,7 @@ async function getProcessBlob(blobId, key, options) {
}
}
return process;
}
/**
* Get or create a process for an app blob.
* @param blobId The blob identifier.
* @param key A unique key for the invocation.
* @param options Other options.
* @return The process.
*/
exports.getProcessBlob = getProcessBlob;
};
/**
* Send any changed account information.

2
deps/c-ares vendored

File diff suppressed because one or more lines are too long

View File

@@ -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.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.3.tgz",
"integrity": "sha512-ZR32LYnPMpf7XZcrYJpSrHJUHNZPTj73/amTtZLhAwzYhSKiDI2OZmCiXbTRvxL1T8X7QTHnCG+KfnRJvH/QsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -307,9 +307,9 @@
}
},
"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"

4
docs/app_development.md Normal file
View File

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

View File

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

View File

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

View File

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

5
docs/howto.md Normal file
View File

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

View File

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

View File

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

5
docs/overview.md Normal file
View File

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

View File

@@ -1,4 +1,4 @@
# How to Transfer an Account
@page transfer_account How to Transfer an Account
Secure Scuttlebutt accounts can be easily transferred between apps and devices.

View File

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

View File

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

6
package-lock.json generated
View File

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

View File

@@ -9,6 +9,12 @@
#include <quickjs.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _app_path_pair_t
{
const char* app;
@@ -91,40 +97,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 +377,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 +491,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 +594,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 +785,440 @@ 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 JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -831,6 +1254,15 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
{
JS_SetPropertyStr(context, core, "globalSettingsDescriptions", JS_NewCFunction(context, _tf_ssb_globalSettingsDescriptions, "globalSettingsDescriptions", 0));
JS_SetPropertyStr(context, core, "globalSettingsGet", JS_NewCFunction(context, _tf_ssb_globalSettingsGet, "globalSettingsGet", 1));
JS_SetPropertyStr(context, core, "globalSettingsSet", JS_NewCFunctionData(context, _tf_ssb_globalSettingsSet, 2, 0, 1, &process));
JS_SetPropertyStr(context, core, "deleteUser", JS_NewCFunctionData(context, _tf_ssb_delete_user, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "addBlock", JS_NewCFunctionData(context, _tf_ssb_add_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "removeBlock", JS_NewCFunctionData(context, _tf_ssb_remove_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getBlocks", JS_NewCFunctionData(context, _tf_ssb_get_blocks, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "swapWithServerIdentity", JS_NewCFunctionData(context, _tf_ssb_swap_with_server_identity, 1, 0, 1, &process));
}
JS_FreeValue(context, administration);
JS_FreeValue(context, permissions);

View File

@@ -950,11 +950,19 @@ void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, con
copy[9] = (low >> 0) & 0xff;
header += 9;
}
memcpy(copy + header, data, size);
if (size)
{
memcpy(copy + header, data, size);
}
_http_write(request->connection, copy, header + size);
tf_free(copy);
}
void tf_http_request_websocket_close(tf_http_request_t* request)
{
_http_connection_destroy(request->connection, "websocket close");
}
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length)
{
if (request->connection->is_response_sent)

View File

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

View File

@@ -218,11 +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)
@@ -233,13 +236,22 @@ static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
static void _httpd_app_kill_task(app_t* work)
{
if (work->taskstub)
JSContext* context = work->request->context;
if (JS_IsObject(work->process))
{
JSContext* context = work->request->context;
JSValue result = tf_taskstub_kill(work->taskstub);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
work->taskstub = NULL;
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);
}
}
@@ -355,10 +367,10 @@ static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this
JSValue process_app = JS_GetPropertyStr(context, app->process, "app");
JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
JS_SetPropertyStr(context, process_app, "_on_output", on_output);
JS_FreeValue(context, process_app);
JSValue send = JS_GetPropertyStr(context, process_app, "send");
JSValue result = JS_Call(context, send, process_app, 0, NULL);
JS_FreeValue(context, process_app);
JS_FreeValue(context, send);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
@@ -501,6 +513,7 @@ static void _httpd_app_message_hello(app_t* work, JSValue message)
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");
@@ -561,6 +574,8 @@ static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const
{
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 */
@@ -572,14 +587,13 @@ static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const
if (JS_IsException(message) || !JS_IsObject(message))
{
tf_util_report_error(context, message);
_httpd_app_kill_task(work);
/* http close? */
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->taskstub && strcmp(action_string, "hello") == 0)
if (action_string && !work->got_hello && strcmp(action_string, "hello") == 0)
{
_httpd_app_message_hello(work, message);
}
@@ -604,6 +618,13 @@ 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)
{
JSContext* context = request->context;
@@ -614,11 +635,31 @@ static void _httpd_app_on_close(tf_http_request_t* request)
JS_FreeValue(context, work->process);
JS_FreeValue(context, work->opaque);
work->process = JS_UNDEFINED;
tf_free(work);
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;
@@ -701,7 +742,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);
@@ -712,7 +754,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");
@@ -740,69 +782,9 @@ static void _tf_httpd_endpoint_app_socket_c(tf_http_request_t* request)
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
}
static void _tf_httpd_endpoint_app_socket_js(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response,
};
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
static bool checked_env;
static bool use_c;
if (!checked_env)
{
char buffer[8] = { 0 };
size_t buffer_size = sizeof(buffer);
use_c = uv_os_getenv("TF_APP_C", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
checked_env = true;
}
if (use_c)
{
_tf_httpd_endpoint_app_socket_c(request);
}
else
{
_tf_httpd_endpoint_app_socket_js(request);
}
}

View File

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

View File

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

View File

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

View File

@@ -252,117 +252,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;
@@ -2172,129 +2061,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,7 +2085,6 @@ 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));
@@ -2350,9 +2115,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object_internal, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object_internal, "addEventListener", JS_NewCFunction(context, _tf_ssb_add_event_listener, "addEventListener", 2));
JS_SetPropertyStr(context, object_internal, "removeEventListener", JS_NewCFunction(context, _tf_ssb_remove_event_listener, "removeEventListener", 2));
JS_SetPropertyStr(context, object_internal, "addBlock", JS_NewCFunction(context, _tf_ssb_add_block, "addBlock", 1));
JS_SetPropertyStr(context, object_internal, "removeBlock", JS_NewCFunction(context, _tf_ssb_remove_block, "removeBlock", 1));
JS_SetPropertyStr(context, object_internal, "getBlocks", JS_NewCFunction(context, _tf_ssb_get_blocks, "getBlocks", 0));
JS_FreeValue(context, global);
}

View File

@@ -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

View File

@@ -227,14 +227,12 @@ bool tf_util_report_error(JSContext* context, JSValue value)
tf_printf("ERROR: %s\n", string);
JS_FreeCString(context, string);
JSValue stack = JS_GetPropertyStr(context, value, "stack");
if (!JS_IsUndefined(stack))
const char* stack = tf_util_get_property_as_string(context, value, "stack");
if (stack && *stack)
{
const char* stack_str = JS_ToCString(context, stack);
tf_printf("%s\n", stack_str);
JS_FreeCString(context, stack_str);
tf_printf("%s\n", stack);
}
JS_FreeValue(context, stack);
JS_FreeCString(context, stack);
tf_task_send_error_to_parent(task, value);
is_error = true;