Compare commits
32 Commits
7c1931f529
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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
|
||||
|
||||
@@ -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": "&Gq9oYdYRgeFSi5TbP/K8xRFtRmcRFmKgnbsEgDzEDoE=.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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()}☰
|
||||
</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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
188
core/app.js
188
core/app.js
@@ -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, {});
|
||||
};
|
||||
|
||||
/** @} */
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
77
core/core.js
77
core/core.js
@@ -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
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
12
deps/codemirror_src/package-lock.json
generated
vendored
12
deps/codemirror_src/package-lock.json
generated
vendored
@@ -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
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,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.
|
||||
|
||||
|
||||
@@ -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
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"
|
||||
|
||||
516
src/api.js.c
516
src/api.js.c
@@ -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);
|
||||
|
||||
10
src/http.c
10
src/http.c
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
136
src/httpd.app.c
136
src/httpd.app.c
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
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);
|
||||
|
||||
/** @} */
|
||||
|
||||
238
src/ssb.js.c
238
src/ssb.js.c
@@ -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);
|
||||
}
|
||||
|
||||
20
src/tests.c
20
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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user