29 Commits

Author SHA1 Message Date
8a93cdd33c Let's release 0.0.20. 2024-06-26 20:29:07 -04:00
92b31de4a9 Latest libbacktrace. 2024-06-26 20:20:41 -04:00
5452f3f623 Appease -fsanitize. 2024-06-26 20:20:34 -04:00
256614dbaf Actually stop stomping settings. 2024-06-26 19:58:59 -04:00
049449b213 I think this is how I lost settings. 2024-06-26 19:44:45 -04:00
85b46336b1 Better draft cleanup on submit. 2024-06-26 19:30:58 -04:00
590afa7b01 Fix content warnings. 2024-06-26 19:27:15 -04:00
574292b798 Reduce some common log noise. 2024-06-23 15:11:18 -04:00
21cf503a59 Fix a navigation bar option I neglected to button-ify. 2024-06-23 11:47:12 -04:00
3630cdbfe0 Consolidate the acount/login navigation bar options to try to save some space on mobile. 2024-06-20 20:41:27 -04:00
0f3be229e6 Actually, let's minify this thing using svgomg. 2024-06-20 20:07:58 -04:00
8e5a024d3d SVG favicon. 2024-06-20 20:05:00 -04:00
410bb7c09d Fix a ref count mistake and add a long-overdue tf_util_print_backtrace() that helped me find it. 2024-06-20 19:49:21 -04:00
9de8b0f449 Oops. 2024-06-20 12:36:21 -04:00
d47c3a1222 Fix a ref/unref mismatch. 2024-06-17 21:45:51 -04:00
df99b3aa90 Trying to catch an issue I think I saw in the debugger. 2024-06-17 21:23:48 -04:00
0090850e10 Forgot the other end of blobs.get. 2024-06-17 20:59:25 -04:00
9efd64bd18 Actually enforce _tf_ssb_assert_not_main_thread. 2024-06-17 12:36:54 -04:00
b16c37e48b Make ssb.privateMessageDecrypt do its work not on the main thread. I think that's finally everything for real. 2024-06-16 17:22:26 -04:00
3ee2c00726 Build fix. 2024-06-16 17:08:10 -04:00
d5a7e19f1a Move the bulk of ssb.privateMessageEncrypt work (CPU + DB) off the main thread. 2024-06-16 17:07:12 -04:00
9b52415b35 Make ssb.setServerFollowingMe not use the DB from the main thread. Two left?? 2024-06-16 16:22:59 -04:00
dbe24494d9 Remove ssb.messageContentGet. It's easy to do this with ssb.sqlAsync, and this wasn't being used productively. Three uses of DB on the main thread remaining. 2024-06-16 16:02:39 -04:00
3eab5a5f70 Make ssb.forgetStoredConnection not use the DB on the main thread. Four remaining? 2024-06-16 15:57:19 -04:00
548febfb22 Make ssb.storedConnections do its DB work not on the main thread. Five remaining by my new count? 2024-06-16 15:29:59 -04:00
b40f72443a A little format, as a treat. 2024-06-16 12:18:19 -04:00
2c03496373 Make databases.list, database.remove, and database.getLike all do their DB work off the main thread. That's the last thing I'm aware of. 2024-06-16 12:17:51 -04:00
b6a937c954 Move db.exchange DB work off of the main thread. 2024-06-16 10:16:39 -04:00
63776d40bd Update codemirror. 2024-06-16 09:23:14 -04:00
28 changed files with 950 additions and 475 deletions

View File

@ -4,8 +4,8 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 20 VERSION_CODE := 20
VERSION_NUMBER := 0.0.20-wip VERSION_NUMBER := 0.0.20
VERSION_NAME := One word all lowercase four words all uppercase. VERSION_NAME := They gave you a small cup.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460000.zip SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460000.zip
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&z0N6jlqflRd4+grj16K/IdllNVLQrPLbr7aKVs/21mE=.sha256" "previous": "&TqpkOAi38Oi6gW6guh95KIvWY2M/vjBE8NLLNHK+M00=.sha256"
} }

View File

@ -76,15 +76,9 @@ class TfComposeElement extends LitElement {
let preview = this.renderRoot.getElementById('preview'); let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.innerText); preview.innerHTML = this.process_text(edit.innerText);
let content_warning = this.renderRoot.getElementById('content_warning'); let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById(
'content_warning_preview'
);
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
let draft = this.get_draft(); let draft = this.get_draft();
draft.text = edit.innerText; draft.text = edit.innerText;
draft.content_warning = content_warning?.innerText; draft.content_warning = content_warning?.value;
setTimeout(() => this.notify(draft), 0); setTimeout(() => this.notify(draft), 0);
} }
@ -221,12 +215,8 @@ class TfComposeElement extends LitElement {
console.log('encrypted as', message); console.log('encrypted as', message);
} }
try { try {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () { await tfrpc.rpc.appendMessage(this.whoami, message);
edit.innerText = '';
self.input();
self.notify(undefined); self.notify(undefined);
self.requestUpdate();
});
} catch (error) { } catch (error) {
alert(error.message); alert(error.message);
} }
@ -459,7 +449,7 @@ class TfComposeElement extends LitElement {
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input> <input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<label for="cw">CW</label> <label for="cw">CW</label>
</p> </p>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input> <input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
</div> </div>
`; `;
} else { } else {

View File

@ -3,7 +3,7 @@
<head> <head>
<title>Tilde Friends Sign-in</title> <title>Tilde Friends Sign-in</title>
<link type="text/css" rel="stylesheet" href="/static/style.css" /> <link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" /> <link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body> <body>

View File

@ -118,28 +118,6 @@ class TfNavigationElement extends LitElement {
return this.spark_lines[key]; return this.spark_lines[key];
} }
/**
* TODOC
* @returns
*/
render_login() {
if (this?.credentials?.session?.name) {
return html`<a
class="w3-bar-item w3-right"
id="login"
href="/login/logout?return=${url() + hash()}"
>logout ${this.credentials.session.name}</a
>`;
} else {
return html`<a
class="w3-bar-item w3-right"
id="login"
href="/login?return=${url() + hash()}"
>login</a
>`;
}
}
set_active_identity(id) { set_active_identity(id) {
send({action: 'setActiveIdentity', identity: id}); send({action: 'setActiveIdentity', identity: id});
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show'); this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
@ -159,8 +137,14 @@ class TfNavigationElement extends LitElement {
window.location.href = '/~core/ssb/#' + this.identity; window.location.href = '/~core/ssb/#' + this.identity;
} }
logout() {
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
}
render_identity() { render_identity() {
let self = this; let self = this;
if (this?.credentials?.session?.name) {
if (this.identities?.length) { if (this.identities?.length) {
return html` return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" /> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
@ -168,6 +152,7 @@ class TfNavigationElement extends LitElement {
<button <button
class="w3-button w3-rest w3-cyan" class="w3-button w3-rest w3-cyan"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
id="identity"
@click=${self.toggle_id_dropdown} @click=${self.toggle_id_dropdown}
> >
${self.names[this.identity]} ${self.names[this.identity]}
@ -202,6 +187,13 @@ class TfNavigationElement extends LitElement {
</button> </button>
` `
)} )}
<button
class="w3-bar-item w3-button w3-border"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
</div> </div>
</div> </div>
`; `;
@ -211,14 +203,39 @@ class TfNavigationElement extends LitElement {
) { ) {
return html` return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" /> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
<button
class="w3-bar-item w3-button w3-right w3-cyan"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
<button <button
id="create_identity" id="create_identity"
@click=${this.create_identity} @click=${this.create_identity}
class="w3-button w3-mobile w3-blue w3-right" class="w3-button w3-mobile w3-red w3-right"
> >
Create an Identity Create an Identity
</button> </button>
`; `;
} else {
return html`
<button
class="w3-bar-item w3-button w3-right w3-cyan"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
`;
}
} else {
return html`<a
class="w3-bar-item w3-cyan w3-right"
id="login"
href="/login?return=${url() + hash()}"
>login</a
>`;
} }
} }
@ -358,7 +375,7 @@ class TfNavigationElement extends LitElement {
${Object.keys(this.spark_lines) ${Object.keys(this.spark_lines)
.sort() .sort()
.map((x) => this.spark_lines[x])} .map((x) => this.spark_lines[x])}
${this.render_login()} ${this.render_identity()} ${this.render_identity()}
</div> </div>
${this.status?.is_error ${this.status?.is_error
? html` ? html`

View File

@ -512,7 +512,7 @@ async function getProcessBlob(blobId, key, options) {
print('Done.'); print('Done.');
}; };
imports.core.deleteUser = async function (user) { imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user') await imports.core.permissionTest('delete_user');
let db = new Database('auth'); let db = new Database('auth');
db.remove('user:' + user); db.remove('user:' + user);
let users = new Set(); let users = new Set();
@ -749,7 +749,7 @@ async function getProcessBlob(blobId, key, options) {
}; };
process.task.setImports(imports); process.task.setImports(imports);
process.task.activate(); process.task.activate();
let source = await getBlobOrContent(blobId); let source = await ssb.blobGet(blobId);
let appSourceName = blobId; let appSourceName = blobId;
let appSource = utf8Decode(source); let appSource = utf8Decode(source);
try { try {
@ -757,7 +757,7 @@ async function getProcessBlob(blobId, key, options) {
if (appObject.type == 'tildefriends-app') { if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js'; appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName]; let id = appObject.files[appSourceName];
let blob = await getBlobOrContent(id); let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob); appSource = utf8Decode(blob);
await process.task.loadFile([ await process.task.loadFile([
'/tfrpc.js', '/tfrpc.js',
@ -767,7 +767,7 @@ async function getProcessBlob(blobId, key, options) {
Object.keys(appObject.files).map(async function (f) { Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([ await process.task.loadFile([
f, f,
await getBlobOrContent(appObject.files[f]), await ssb.blobGet(appObject.files[f]),
]); ]);
}) })
); );
@ -851,21 +851,6 @@ function sendData(response, data, type, headers, status_code) {
} }
} }
/**
* TODOC
* @param {*} id
* @returns
*/
async function getBlobOrContent(id) {
if (!id) {
return;
} else if (id.startsWith('&')) {
return ssb.blobGet(id);
} else if (id.startsWith('%')) {
return ssb.messageContentGet(id);
}
}
let g_handler_index = 0; let g_handler_index = 0;
/** /**
@ -993,7 +978,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers); response.writeHead(304, headers);
response.end(); response.end();
} else { } else {
data = await getBlobOrContent(id); data = await ssb.blobGet(id);
if (match[3]) { if (match[3]) {
let appObject = JSON.parse(data); let appObject = JSON.parse(data);
data = appObject.files[match[3]]; data = appObject.files[match[3]];
@ -1025,7 +1010,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers); response.writeHead(304, headers);
response.end(); response.end();
} else { } else {
data = await getBlobOrContent(blobId); data = await ssb.blobGet(blobId);
sendData( sendData(
response, response,
data, data,
@ -1141,7 +1126,7 @@ async function blobHandler(request, response, blobId, uri) {
app_id = await db.get('path:' + match[2]); app_id = await db.get('path:' + match[2]);
} }
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id))); let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
id = app_object?.files[uri.substring(1)]; id = app_object?.files[uri.substring(1)];
if (!id && app_object?.files['handler.js']) { if (!id && app_object?.files['handler.js']) {
let answer; let answer;
@ -1197,7 +1182,7 @@ async function blobHandler(request, response, blobId, uri) {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy, 'Content-Security-Policy': k_content_security_policy,
}; };
data = await getBlobOrContent(id); data = await ssb.blobGet(id);
let type = let type =
httpd.mime_type_from_extension(uri) || httpd.mime_type_from_extension(uri) ||
httpd.mime_type_from_magic_bytes(data); httpd.mime_type_from_magic_bytes(data);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

View File

@ -4,7 +4,7 @@
<title>Tilde Friends</title> <title>Tilde Friends</title>
<link type="text/css" rel="stylesheet" href="/static/style.css" /> <link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="text/css" rel="stylesheet" href="/static/w3.css" /> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" /> <link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script> <script>
function set_access_key_title(event) { function set_access_key_title(event) {

1
core/tildefriends.svg Normal file
View File

@ -0,0 +1 @@
<svg width="65" height="65" viewBox="0 0 61 65" fill="none" xmlns="http://www.w3.org/2000/svg"><path style="fill:#0af;stroke-width:.712717;fill-opacity:1" d="M6 0h49a8 8 45 0 1 8 8v49a8 8 135 0 1-8 8H6a8 8 45 0 1-8-8V8a8 8 135 0 1 8-8Z"/><text xml:space="preserve" style="font-style:normal;font-weight:400;font-size:40px;line-height:1.25;font-family:sans-serif;fill:#000;fill-opacity:1;stroke:none" x="-.023" y="47.568"><tspan x="-.023" y="47.568" style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:40px;font-family:Arial;-inkscape-font-specification:'Arial, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">~</tspan></text><g transform="translate(16.213 5.975) scale(.72923)"><circle cx="36" cy="36" r="23" fill="#fcea2b"/><path fill="#3f3f3f" d="M45.331 38.564c3.963 0 7.178-2.862 7.178-6.389 0-1.765.448-3.53-.852-4.685-1.299-1.156-4.345-1.704-6.326-1.704-2.357 0-5.143.143-6.451 1.704-.894 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389zM25.738 38.564c3.963 0 7.179-2.862 7.179-6.389 0-1.765.447-3.53-.852-4.685-1.3-1.156-4.345-1.704-6.327-1.704-2.356 0-5.142.143-6.451 1.704-.893 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389z"/></g><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" transform="translate(16.213 5.975) scale(.72923)"><circle cx="35.887" cy="36.056" r="23"/><path d="M45.702 44.862c-6.574 3.525-14.045 3.658-19.63 0M18.883 30.464s-.953 8.55 6.86 7.918c2.62-.212 7.817-.65 7.867-8.342.005-.698-.007-1.6-.81-2.63-1.065-1.367-3.572-1.971-9.945-1.422 0 0-3.446-.1-3.972 4.476z"/><path d="m18.953 29.931-.433-3.372 3.833-.527M52.741 30.464s.953 8.55-6.86 7.918c-2.62-.212-7.817-.65-7.868-8.342-.004-.698.008-1.6.811-2.63 1.065-1.367 3.572-1.971 9.945-1.422 0 0 3.446-.1 3.972 4.476z"/><path d="M31.505 26.416s4.124 2.534 8.657 0M33.536 31.318s2.202-3.751 4.536 0M52.664 29.933l.433-3.371-3.833-.528"/><path d="M33.955 30.027s1.795-3.75 3.699 0"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -21,7 +21,7 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.0.19"; version = "0.0.20";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";

File diff suppressed because one or more lines are too long

View File

@ -19,9 +19,9 @@
} }
}, },
"node_modules/@codemirror/autocomplete": { "node_modules/@codemirror/autocomplete": {
"version": "6.16.0", "version": "6.16.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz",
"integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==", "integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -36,13 +36,13 @@
} }
}, },
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.5.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
"integrity": "sha512-rK+sj4fCAN/QfcY9BEzYMgp4wwL/q5aj/VfNSoH1RWPF9XS/dUwBkvlL3hpWgEjOqlpdN1uLC9UkjJ4tmyjJYg==", "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0" "@lezer/common": "^1.1.0"
} }
}, },
@ -98,9 +98,9 @@
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.1", "version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -147,9 +147,9 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.26.3", "version": "6.28.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.1.tgz",
"integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==", "integrity": "sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
@ -238,9 +238,9 @@
} }
}, },
"node_modules/@lezer/html": { "node_modules/@lezer/html": {
"version": "1.3.9", "version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.9.tgz", "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==", "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@ -248,9 +248,9 @@
} }
}, },
"node_modules/@lezer/javascript": { "node_modules/@lezer/javascript": {
"version": "1.4.16", "version": "1.4.17",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.16.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz",
"integrity": "sha512-84UXR3N7s11MPQHWgMnjb9571fr19MmXnr5zTv2XX0gHXXUvW3uPJ8GCjKrfTXmSdfktjRK0ayKklw+A13rk4g==", "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
@ -268,9 +268,9 @@
} }
}, },
"node_modules/@lezer/lr": { "node_modules/@lezer/lr": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@ -545,9 +545,9 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.11.3", "version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -819,9 +819,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.31.0", "version": "5.31.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz",
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends" package="com.unprompted.tildefriends"
android:versionCode="20" android:versionCode="20"
android:versionName="0.0.20-wip"> android:versionName="0.0.20">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/> <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<application <application

View File

@ -51,7 +51,7 @@ void tf_database_register(JSContext* context)
JS_SetPropertyStr(context, global, "Database", constructor); JS_SetPropertyStr(context, global, "Database", constructor);
JSValue databases = JS_NewObject(context); JSValue databases = JS_NewObject(context);
JS_SetPropertyStr(context, global, "databases", databases); JS_SetPropertyStr(context, global, "databases", databases);
JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 0, 0, 0, NULL)); JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 1, 0, 0, NULL));
JS_FreeValue(context, global); JS_FreeValue(context, global);
} }
@ -188,7 +188,7 @@ static void _database_set_work(tf_ssb_t* ssb, void* user_data)
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK)
{ {
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK && if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK) sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{ {
work->result = true; work->result = true;
} }
@ -245,79 +245,146 @@ static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc
return result; return result;
} }
static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) typedef struct _database_exchange_t
{ {
JSValue exchanged = JS_UNDEFINED; const char* id;
database_t* database = JS_GetOpaque(this_val, _database_class_id); const char* key;
if (database) size_t key_length;
{ const char* expected;
sqlite3_stmt* statement; size_t expected_length;
tf_ssb_t* ssb = tf_task_get_ssb(database->task); const char* value;
size_t value_length;
bool result;
JSValue promise[2];
} database_exchange_t;
static void _database_exchange_work(tf_ssb_t* ssb, void* user_data)
{
database_exchange_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) sqlite3_stmt* statement;
if (!work->expected)
{ {
if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
{ {
size_t key_length; if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
size_t set_length; sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, set, set_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{ {
exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE; work->result = sqlite3_changes(db) != 0;
} }
JS_FreeCString(context, key);
JS_FreeCString(context, set);
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
} }
else if (sqlite3_prepare(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK) else if (sqlite3_prepare(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK)
{ {
size_t key_length; if (sqlite3_bind_text(statement, 1, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->id, -1, NULL) == SQLITE_OK &&
size_t expected_length; sqlite3_bind_text(statement, 3, work->key, work->key_length, NULL) == SQLITE_OK &&
size_t set_length; sqlite3_bind_text(statement, 4, work->expected, work->expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
const char* expected = JS_ToCStringLen(context, &expected_length, argv[1]);
const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
if (sqlite3_bind_text(statement, 1, set, set_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, database->id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, key, key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 4, expected, expected_length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE)
{ {
exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE; work->result = sqlite3_changes(db) != 0;
} }
JS_FreeCString(context, key);
JS_FreeCString(context, expected);
JS_FreeCString(context, set);
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_writer(ssb, db); tf_ssb_release_db_writer(ssb, db);
}
static void _database_exchange_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_exchange_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->key);
JS_FreeCString(context, work->expected);
JS_FreeCString(context, work->value);
tf_free((char*)work->id);
tf_free(work);
}
static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
database_exchange_t* work = tf_malloc(sizeof(database_exchange_t));
*work = (database_exchange_t) {
.id = tf_strdup(database->id),
};
work->key = JS_ToCStringLen(context, &work->key_length, argv[0]);
work->expected = (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) ? NULL : JS_ToCStringLen(context, &work->expected_length, argv[1]);
work->value = JS_ToCStringLen(context, &work->value_length, argv[2]);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_exchange_work, _database_exchange_after_work, work);
} }
return exchanged; return result;
}
typedef struct _database_remove_t
{
const char* id;
size_t key_length;
JSValue promise[2];
char key[];
} database_remove_t;
static void _database_remove_work(tf_ssb_t* ssb, void* user_data)
{
database_remove_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_OK)
{
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _database_remove_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_remove_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((char*)work->id);
tf_free(work);
} }
static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id); database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database) if (database)
{ {
sqlite3_stmt* statement; size_t key_length = 0;
tf_ssb_t* ssb = tf_task_get_ssb(database->task); const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK) database_remove_t* work = tf_malloc(sizeof(database_remove_t) + key_length + 1);
{ *work = (database_remove_t) {
size_t keyLength; .id = tf_strdup(database->id),
const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]); .key_length = key_length,
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK && };
sqlite3_step(statement) == SQLITE_OK) memcpy(work->key, key, key_length + 1);
{ JS_FreeCString(context, key);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(tf_task_get_ssb(database->task), _database_remove_work, _database_remove_after_work, work);
} }
JS_FreeCString(context, keyString); return result;
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
return JS_UNDEFINED;
} }
typedef struct _database_get_all_t typedef struct _database_get_all_t
@ -406,57 +473,153 @@ static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int
return result; return result;
} }
typedef struct _key_value_t
{
char* key;
size_t key_length;
char* value;
size_t value_length;
} key_value_t;
typedef struct _database_get_like_t
{
const char* id;
const char* pattern;
key_value_t* results;
int results_length;
JSValue promise[2];
} database_get_like_t;
static void _database_get_like_work(tf_ssb_t* ssb, void* user_data)
{
database_get_like_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->pattern, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
work->results = tf_resize_vec(work->results, sizeof(key_value_t) * (work->results_length + 1));
key_value_t* out = &work->results[work->results_length];
*out = (key_value_t) {
.key_length = sqlite3_column_bytes(statement, 0),
.value_length = sqlite3_column_bytes(statement, 1),
};
out->key = tf_malloc(out->key_length + 1);
memcpy(out->key, sqlite3_column_text(statement, 0), out->key_length + 1);
out->value = tf_malloc(out->value_length + 1);
memcpy(out->value, sqlite3_column_text(statement, 1), out->value_length + 1);
work->results_length++;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_like_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_like_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewObject(context);
for (int i = 0; i < work->results_length; i++)
{
const key_value_t* row = &work->results[i];
JS_SetPropertyStr(context, result, row->key, JS_NewStringLen(context, row->value, row->value_length));
tf_free(row->key);
tf_free(row->value);
}
JS_FreeCString(context, work->pattern);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->id);
tf_free(work->results);
tf_free(work);
}
static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id); database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database) if (database)
{ {
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task); tf_ssb_t* ssb = tf_task_get_ssb(database->task);
database_get_like_t* work = tf_malloc(sizeof(database_get_like_t));
*work = (database_get_like_t) {
.id = tf_strdup(database->id),
.pattern = JS_ToCString(context, argv[0]),
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_get_like_work, _database_get_like_after_work, work);
}
return result;
}
typedef struct _databases_list_t
{
const char* pattern;
char** names;
int names_length;
JSValue promise[2];
} databases_list_t;
static void _databases_list_work(tf_ssb_t* ssb, void* user_data)
{
databases_list_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK) sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{ {
const char* pattern = JS_ToCString(context, argv[0]); if (sqlite3_bind_text(statement, 1, work->pattern, -1, NULL) == SQLITE_OK)
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, pattern, -1, NULL) == SQLITE_OK)
{ {
result = JS_NewObject(context);
while (sqlite3_step(statement) == SQLITE_ROW) while (sqlite3_step(statement) == SQLITE_ROW)
{ {
JS_SetPropertyStr(context, result, (const char*)sqlite3_column_text(statement, 0), work->names = tf_resize_vec(work->names, sizeof(char*) * (work->names_length + 1));
JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 1), sqlite3_column_bytes(statement, 1))); work->names[work->names_length] = tf_strdup((const char*)sqlite3_column_text(statement, 0));
work->names_length++;
} }
} }
JS_FreeCString(context, pattern);
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
}
static void _databases_list_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
databases_list_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->names_length; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->names[i]));
tf_free(work->names[i]);
} }
return result; JS_FreeCString(context, work->pattern);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->names);
tf_free(work);
} }
static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data) static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{ {
tf_task_t* task = tf_task_get(context); tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task); tf_ssb_t* ssb = tf_task_get_ssb(task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb); databases_list_t* work = tf_malloc(sizeof(databases_list_t));
JSValue array = JS_UNDEFINED; *work = (databases_list_t) {
sqlite3_stmt* statement; .pattern = JS_ToCString(context, argv[0]),
if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK) };
{ JSValue result = JS_NewPromiseCapability(context, work->promise);
const char* pattern = JS_ToCString(context, argv[0]); tf_ssb_run_work(ssb, _databases_list_work, _databases_list_after_work, work);
if (sqlite3_bind_text(statement, 1, pattern, -1, NULL) == SQLITE_OK) return result;
{
array = JS_NewArray(context);
uint32_t index = 0;
while (sqlite3_step(statement) == SQLITE_ROW)
{
JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)));
}
}
JS_FreeCString(context, pattern);
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return array;
} }

View File

@ -1019,7 +1019,7 @@ void tf_http_request_unref(tf_http_request_t* request)
tf_free(request); tf_free(request);
} }
if (--connection->ref_count == 0) if (connection && --connection->ref_count == 0)
{ {
if (connection->http->is_shutting_down) if (connection->http->is_shutting_down)
{ {

View File

@ -884,7 +884,7 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
const char* k_static_files[] = { const char* k_static_files[] = {
"index.html", "index.html",
"client.js", "client.js",
"favicon.png", "tildefriends.svg",
"jszip.min.js", "jszip.min.js",
"style.css", "style.css",
"tfrpc.js", "tfrpc.js",
@ -1351,11 +1351,10 @@ static bool _verify_password(const char* password, const char* hash)
return out_hash && strcmp(hash, out_hash) == 0; return out_hash && strcmp(hash, out_hash) == 0;
} }
static bool _make_administrator_if_first(tf_ssb_t* ssb, const char* account_name_copy, bool may_become_first_admin) static bool _make_administrator_if_first(tf_ssb_t* ssb, JSContext* context, const char* account_name_copy, bool may_become_first_admin)
{ {
JSContext* context = tf_ssb_get_context(ssb);
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings"); const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED; JSValue settings_value = settings && *settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
if (JS_IsUndefined(settings_value)) if (JS_IsUndefined(settings_value))
{ {
settings_value = JS_NewObject(context); settings_value = JS_NewObject(context);
@ -1523,7 +1522,7 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
tf_free(post_form_data); tf_free(post_form_data);
} }
bool have_administrator = _make_administrator_if_first(ssb, account_name_copy, may_become_first_admin); bool have_administrator = _make_administrator_if_first(ssb, context, account_name_copy, may_become_first_admin);
if (session_is_new && _form_data_get(form_data, "return") && !login_error) if (session_is_new && _form_data_get(form_data, "return") && !login_error)
{ {
@ -1541,7 +1540,6 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
} }
else else
{ {
tf_http_request_ref(request);
login->name = account_name_copy; login->name = account_name_copy;
login->error = login_error; login->error = login_error;
@ -1566,6 +1564,7 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
} }
login->pending++; login->pending++;
tf_http_request_ref(request);
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login); tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
account_name_copy = NULL; account_name_copy = NULL;
@ -1584,9 +1583,9 @@ done:
static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{ {
login_request_t* login = user_data; login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (login->pending == 1) if (login->pending == 1)
{ {
tf_http_request_t* request = login->request;
if (*login->location_header) if (*login->location_header)
{ {
const char* headers[] = { const char* headers[] = {
@ -1597,8 +1596,8 @@ static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* us
}; };
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0); tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
} }
tf_http_request_unref(request);
} }
tf_http_request_unref(request);
_login_release(login); _login_release(login);
} }

View File

@ -1674,10 +1674,6 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
tf_trace_end(connection->ssb->trace); tf_trace_end(connection->ssb->trace);
} }
} }
else
{
tf_printf("No request callback for %p %d\n", connection, -request_number);
}
} }
if (close_connection) if (close_connection)
@ -2270,6 +2266,7 @@ static void _tf_ssb_assert_not_main_thread(tf_ssb_t* ssb)
const char* bt = tf_util_backtrace_string(); const char* bt = tf_util_backtrace_string();
tf_printf("Acquiring DB from the main thread:\n%s\n", bt); tf_printf("Acquiring DB from the main thread:\n%s\n", bt);
tf_free((void*)bt); tf_free((void*)bt);
abort();
} }
} }
@ -3759,7 +3756,12 @@ void tf_ssb_ref(tf_ssb_t* ssb)
void tf_ssb_unref(tf_ssb_t* ssb) void tf_ssb_unref(tf_ssb_t* ssb)
{ {
ssb->ref_count--; int new_count = --ssb->ref_count;
if (new_count < 0)
{
tf_printf("tf_ssb_unref past 0: %d\n", new_count);
abort();
}
} }
void tf_ssb_set_main_thread(tf_ssb_t* ssb, bool main_thread) void tf_ssb_set_main_thread(tf_ssb_t* ssb, bool main_thread)

View File

@ -281,7 +281,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
tf_ssb_release_db_writer(ssb, db); tf_ssb_release_db_writer(ssb, db);
} }
static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int64_t sequence, const char* previous) static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int64_t sequence, const char* previous, bool* out_id_mismatch)
{ {
bool exists = false; bool exists = false;
if (sequence == 1) if (sequence == 1)
@ -291,12 +291,13 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
else else
{ {
sqlite3_stmt* statement; sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT COUNT(*) FROM messages WHERE author = ?1 AND sequence = ?2 AND id = ?3", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "SELECT COUNT(*), id != ?3 AS is_mismatch FROM messages WHERE author = ?1 AND sequence = ?2", -1, &statement, NULL) == SQLITE_OK)
{ {
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, sequence - 1) == SQLITE_OK && if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, sequence - 1) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{ {
exists = sqlite3_column_int(statement, 0) != 0; exists = sqlite3_column_int(statement, 0) != 0;
*out_id_mismatch = sqlite3_column_int(statement, 1) != 0;
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
@ -309,8 +310,9 @@ static int64_t _tf_ssb_db_store_message_raw(tf_ssb_t* ssb, const char* id, const
{ {
sqlite3* db = tf_ssb_acquire_db_writer(ssb); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
int64_t last_row_id = -1; int64_t last_row_id = -1;
bool id_mismatch = false;
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous)) if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
{ {
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), " const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING"; "?, ?, ?) ON CONFLICT DO NOTHING";
@ -345,8 +347,14 @@ static int64_t _tf_ssb_db_store_message_raw(tf_ssb_t* ssb, const char* id, const
tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db)); tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
} }
} }
else else if (id_mismatch)
{ {
/*
** Only warn if we find a previous message with the wrong ID.
** If a feed is forked, we would otherwise warn on every
** message when trying to receive what we don't have, and
** that's not helping anybody.
*/
tf_printf("%p: Previous message doesn't exist for author=%s sequence=%" PRId64 " previous=%s.\n", db, author, sequence, previous); tf_printf("%p: Previous message doesn't exist for author=%s sequence=%" PRId64 " previous=%s.\n", db, author, sequence, previous);
} }
tf_ssb_release_db_writer(ssb, db); tf_ssb_release_db_writer(ssb, db);

View File

@ -255,7 +255,7 @@ static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val,
return result; return result;
} }
static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_val, JSValue id, JSValue following) static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_val, const char* id, bool follow)
{ {
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context); JSValue message = JS_NewObject(context);
@ -264,8 +264,8 @@ static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_v
tf_ssb_whoami(ssb, server_id_buffer, sizeof(server_id_buffer)); tf_ssb_whoami(ssb, server_id_buffer, sizeof(server_id_buffer));
JSValue server_id = JS_NewString(context, server_id_buffer); JSValue server_id = JS_NewString(context, server_id_buffer);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact")); JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact"));
JS_SetPropertyStr(context, message, "contact", JS_DupValue(context, id)); JS_SetPropertyStr(context, message, "contact", JS_NewString(context, id));
JS_SetPropertyStr(context, message, "following", JS_DupValue(context, following)); JS_SetPropertyStr(context, message, "following", JS_NewBool(context, follow));
JSValue args[] = { JSValue args[] = {
server_user, server_user,
server_id, server_id,
@ -278,43 +278,84 @@ static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_v
return result; return result;
} }
static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) typedef struct _set_server_following_me_t
{ {
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); const char* user;
JSValue result = JS_UNDEFINED; const char* key;
if (ssb) bool follow;
JSValue this_val;
JSValue promise[2];
bool error_does_not_own_key;
bool append_message;
} set_server_following_me_t;
static void _tf_ssb_set_server_following_me_work(tf_ssb_t* ssb, void* user_data)
{
set_server_following_me_t* work = user_data;
if (!tf_ssb_db_identity_get_private_key(ssb, work->user, work->key, NULL, 0))
{ {
char server_id[k_id_base64_len]; work->error_does_not_own_key = true;
tf_ssb_whoami(ssb, server_id, sizeof(server_id));
const char* user = JS_ToCString(context, argv[0]);
const char* key = JS_ToCString(context, argv[1]);
if (!tf_ssb_db_identity_get_private_key(ssb, user, key, NULL, 0))
{
result = JS_ThrowInternalError(context, "User %s does not own key %s.", user, key);
} }
else else
{ {
char server_id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, server_id, sizeof(server_id));
const char* server_id_ptr = server_id; const char* server_id_ptr = server_id;
const char** current_following = tf_ssb_db_following_deep_ids(ssb, &server_id_ptr, 1, 1); const char** current_following = tf_ssb_db_following_deep_ids(ssb, &server_id_ptr, 1, 1);
bool is_following = false; bool is_following = false;
for (const char** it = current_following; *it; it++) for (const char** it = current_following; *it; it++)
{ {
if (strcmp(key, *it) == 0) if (strcmp(work->key, *it) == 0)
{ {
is_following = true; is_following = true;
break; break;
} }
} }
tf_free(current_following); tf_free(current_following);
work->append_message = (work->follow && !is_following) || (!work->follow && is_following);
}
}
bool want_following = JS_ToBool(context, argv[2]); static void _tf_ssb_set_server_following_me_after_work(tf_ssb_t* ssb, int status, void* user_data)
if ((want_following && !is_following) || (!want_following && is_following)) {
set_server_following_me_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->error_does_not_own_key)
{ {
result = _set_server_following_internal(ssb, this_val, argv[1], argv[2]); result = JS_ThrowInternalError(context, "User %s does not own key %s.", work->user, work->key);
} }
else if (work->append_message)
{
result = _set_server_following_internal(ssb, work->this_val, work->key, work->follow);
} }
JS_FreeCString(context, key); JS_FreeCString(context, work->key);
JS_FreeCString(context, user); JS_FreeCString(context, work->user);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->this_val);
tf_free(work);
}
static JSValue _tf_ssb_set_server_following_me(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)
{
set_server_following_me_t* work = tf_malloc(sizeof(set_server_following_me_t));
*work = (set_server_following_me_t) {
.user = JS_ToCString(context, argv[0]),
.key = JS_ToCString(context, argv[1]),
.follow = JS_ToBool(context, argv[2]),
.this_val = JS_DupValue(context, this_val),
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_set_server_following_me_work, _tf_ssb_set_server_following_me_after_work, work);
} }
return result; return result;
} }
@ -910,25 +951,6 @@ static JSValue _tf_ssb_blobStore(JSContext* context, JSValueConst this_val, int
return result; return result;
} }
static JSValue _tf_ssb_messageContentGet(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_NULL;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb)
{
const char* id = JS_ToCString(context, argv[0]);
uint8_t* blob = NULL;
size_t size = 0;
if (tf_ssb_db_message_content_get(ssb, id, &blob, &size))
{
result = JS_NewArrayBufferCopy(context, blob, size);
tf_free(blob);
}
JS_FreeCString(context, id);
}
return result;
}
static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NULL; JSValue result = JS_NULL;
@ -969,24 +991,53 @@ static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, in
return result; return result;
} }
typedef struct _stored_connections_t
{
int count;
tf_ssb_db_stored_connection_t* connections;
JSValue promise[2];
} stored_connections_t;
static void _tf_ssb_stored_connections_work(tf_ssb_t* ssb, void* user_data)
{
stored_connections_t* work = user_data;
work->connections = tf_ssb_db_get_stored_connections(ssb, &work->count);
}
static void _tf_ssb_stored_connections_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
stored_connections_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 connection = JS_NewObject(context);
JS_SetPropertyStr(context, connection, "address", JS_NewString(context, work->connections[i].address));
JS_SetPropertyStr(context, connection, "port", JS_NewInt32(context, work->connections[i].port));
JS_SetPropertyStr(context, connection, "pubkey", JS_NewString(context, work->connections[i].pubkey));
JS_SetPropertyUint32(context, result, i, connection);
}
tf_free(work->connections);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
tf_free(work);
}
static JSValue _tf_ssb_storedConnections(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_storedConnections(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_NULL; JSValue result = JS_NULL;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb) if (ssb)
{ {
int count = 0; stored_connections_t* work = tf_malloc(sizeof(stored_connections_t));
tf_ssb_db_stored_connection_t* connections = tf_ssb_db_get_stored_connections(ssb, &count); *work = (stored_connections_t) { 0 };
result = JS_NewArray(context); result = JS_NewPromiseCapability(context, work->promise);
for (int i = 0; i < count; i++) tf_ssb_run_work(ssb, _tf_ssb_stored_connections_work, _tf_ssb_stored_connections_after_work, work);
{
JSValue connection = JS_NewObject(context);
JS_SetPropertyStr(context, connection, "address", JS_NewString(context, connections[i].address));
JS_SetPropertyStr(context, connection, "port", JS_NewInt32(context, connections[i].port));
JS_SetPropertyStr(context, connection, "pubkey", JS_NewString(context, connections[i].pubkey));
JS_SetPropertyUint32(context, result, i, connection);
}
tf_free(connections);
} }
return result; return result;
} }
@ -1478,8 +1529,40 @@ static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int ar
return JS_UNDEFINED; return JS_UNDEFINED;
} }
typedef struct _forget_stored_connection_t
{
const char* address;
int32_t port;
const char* pubkey;
JSValue promise[2];
} forget_stored_connection_t;
static void _tf_ssb_forget_stored_connection_work(tf_ssb_t* ssb, void* user_data)
{
forget_stored_connection_t* work = user_data;
if (work->pubkey)
{
tf_ssb_db_forget_stored_connection(ssb, work->address, work->port, work->pubkey);
}
}
static void _tf_ssb_forget_stored_connection_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
forget_stored_connection_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JS_FreeCString(context, work->pubkey);
JS_FreeCString(context, work->address);
JSValue result = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_forgetStoredConnection(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_forgetStoredConnection(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED;
JSValue args = argv[0]; JSValue args = argv[0];
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (ssb) if (ssb)
@ -1491,17 +1574,20 @@ static JSValue _tf_ssb_forgetStoredConnection(JSContext* context, JSValueConst t
int32_t port_int = 0; int32_t port_int = 0;
JS_ToInt32(context, &port_int, port); JS_ToInt32(context, &port_int, port);
const char* pubkey_str = JS_ToCString(context, pubkey); const char* pubkey_str = JS_ToCString(context, pubkey);
if (pubkey_str)
{ forget_stored_connection_t* work = tf_malloc(sizeof(forget_stored_connection_t));
tf_ssb_db_forget_stored_connection(ssb, address_str, port_int, pubkey_str); *work = (forget_stored_connection_t) {
} .address = address_str,
JS_FreeCString(context, pubkey_str); .port = port_int,
JS_FreeCString(context, address_str); .pubkey = pubkey_str,
};
result = JS_NewPromiseCapability(context, work->promise);
JS_FreeValue(context, address); JS_FreeValue(context, address);
JS_FreeValue(context, port); JS_FreeValue(context, port);
JS_FreeValue(context, pubkey); JS_FreeValue(context, pubkey);
tf_ssb_run_work(ssb, _tf_ssb_forget_stored_connection_work, _tf_ssb_forget_stored_connection_after_work, work);
} }
return JS_UNDEFINED; return result;
} }
static void _tf_ssb_cleanup_value(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_cleanup_value(tf_ssb_t* ssb, void* user_data)
@ -1767,6 +1853,142 @@ static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, co
return success; return success;
} }
typedef struct _private_message_encrypt_t
{
const char* signer_user;
const char* signer_identity;
uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES];
int recipient_count;
const char* message;
size_t message_size;
JSValue promise[2];
bool error_id_not_found;
bool error_secretbox_failed;
bool error_scalarmult_failed;
char* encrypted;
size_t encrypted_length;
} private_message_encrypt_t;
static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_encrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(db, work->signer_user, work->signer_identity, private_key);
tf_ssb_release_db_reader(ssb, db);
if (found)
{
uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 };
uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 0 };
uint8_t nonce[crypto_box_NONCEBYTES] = { 0 };
uint8_t body_key[crypto_box_SECRETKEYBYTES] = { 0 };
crypto_box_keypair(public_key, secret_key);
randombytes_buf(nonce, sizeof(nonce));
randombytes_buf(body_key, sizeof(body_key));
uint8_t length_and_key[1 + sizeof(body_key)];
length_and_key[0] = (uint8_t)work->recipient_count;
memcpy(length_and_key + 1, body_key, sizeof(body_key));
size_t payload_size =
sizeof(nonce) + sizeof(public_key) + (crypto_secretbox_MACBYTES + sizeof(length_and_key)) * work->recipient_count + crypto_secretbox_MACBYTES + work->message_size;
uint8_t* payload = tf_malloc(payload_size);
uint8_t* p = payload;
memcpy(p, nonce, sizeof(nonce));
p += sizeof(nonce);
memcpy(p, public_key, sizeof(public_key));
p += sizeof(public_key);
for (int i = 0; i < work->recipient_count; i++)
{
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, secret_key, work->recipients[i]) == 0)
{
if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0)
{
work->error_secretbox_failed = true;
break;
}
else
{
p += crypto_secretbox_MACBYTES + sizeof(length_and_key);
}
}
else
{
work->error_scalarmult_failed = true;
break;
}
}
if (!work->error_secretbox_failed && !work->error_scalarmult_failed)
{
if (crypto_secretbox_easy(p, (const uint8_t*)work->message, work->message_size, nonce, body_key) != 0)
{
work->error_scalarmult_failed = true;
}
else
{
p += crypto_secretbox_MACBYTES + work->message_size;
assert((size_t)(p - payload) == payload_size);
char* encoded = tf_malloc(payload_size * 2 + 5);
size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5);
memcpy(encoded + encoded_length, ".box", 5);
encoded_length += 4;
work->encrypted = encoded;
work->encrypted_length = encoded_length;
}
}
tf_free(payload);
}
else
{
work->error_id_not_found = true;
}
}
static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
private_message_encrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->error_secretbox_failed)
{
result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed");
}
else if (work->error_scalarmult_failed)
{
result = JS_ThrowInternalError(context, "crypto_scalarmult failed");
}
else if (work->error_id_not_found)
{
result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", work->signer_identity, work->signer_user);
}
else
{
result = JS_NewStringLen(context, work->encrypted, work->encrypted_length);
tf_free((void*)work->encrypted);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->signer_user);
JS_FreeCString(context, work->signer_identity);
JS_FreeCString(context, work->message);
tf_free(work);
}
static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
@ -1776,11 +1998,7 @@ static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst
return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients); return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients);
} }
const char* signer_user = JS_ToCString(context, argv[0]); uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES] = { 0 };
const char* signer_identity = JS_ToCString(context, argv[1]);
uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES];
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[3]);
for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++) for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
{ {
JSValue recipient = JS_GetPropertyUint32(context, argv[2], i); JSValue recipient = JS_GetPropertyUint32(context, argv[2], i);
@ -1805,111 +2023,54 @@ static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst
if (JS_IsUndefined(result)) if (JS_IsUndefined(result))
{ {
const char* signer_user = JS_ToCString(context, argv[0]);
const char* signer_identity = JS_ToCString(context, argv[1]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[3]);
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId); tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; private_message_encrypt_t* work = tf_malloc(sizeof(private_message_encrypt_t));
*work = (private_message_encrypt_t) {
sqlite3* db = tf_ssb_acquire_db_reader(ssb); .signer_user = signer_user,
bool found = _tf_ssb_get_private_key_curve25519(db, signer_user, signer_identity, private_key); .signer_identity = signer_identity,
tf_ssb_release_db_reader(ssb, db); .recipient_count = recipient_count,
.message = message,
if (found) .message_size = message_size,
{ };
uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 }; static_assert(sizeof(work->recipients) == sizeof(recipients), "size mismatch");
uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 0 }; memcpy(work->recipients, recipients, sizeof(recipients));
uint8_t nonce[crypto_box_NONCEBYTES] = { 0 }; result = JS_NewPromiseCapability(context, work->promise);
uint8_t body_key[crypto_box_SECRETKEYBYTES] = { 0 }; tf_ssb_run_work(ssb, _tf_ssb_private_message_encrypt_work, _tf_ssb_private_message_encrypt_after_work, work);
crypto_box_keypair(public_key, secret_key);
randombytes_buf(nonce, sizeof(nonce));
randombytes_buf(body_key, sizeof(body_key));
uint8_t length_and_key[1 + sizeof(body_key)];
length_and_key[0] = (uint8_t)recipient_count;
memcpy(length_and_key + 1, body_key, sizeof(body_key));
size_t payload_size =
sizeof(nonce) + sizeof(public_key) + (crypto_secretbox_MACBYTES + sizeof(length_and_key)) * recipient_count + crypto_secretbox_MACBYTES + message_size;
uint8_t* payload = tf_malloc(payload_size);
uint8_t* p = payload;
memcpy(p, nonce, sizeof(nonce));
p += sizeof(nonce);
memcpy(p, public_key, sizeof(public_key));
p += sizeof(public_key);
for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
{
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, secret_key, recipients[i]) == 0)
{
if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0)
{
result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed");
}
else
{
p += crypto_secretbox_MACBYTES + sizeof(length_and_key);
}
}
else
{
result = JS_ThrowInternalError(context, "crypto_scalarmult failed");
}
} }
if (JS_IsUndefined(result))
{
if (crypto_secretbox_easy(p, (const uint8_t*)message, message_size, nonce, body_key) != 0)
{
result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed for the message.\n");
}
else
{
p += crypto_secretbox_MACBYTES + message_size;
assert((size_t)(p - payload) == payload_size);
char* encoded = tf_malloc(payload_size * 2 + 5);
size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5);
memcpy(encoded + encoded_length, ".box", 5);
encoded_length += 4;
result = JS_NewStringLen(context, encoded, encoded_length);
tf_free(encoded);
}
}
tf_free(payload);
}
else
{
result = JS_ThrowInternalError(context, "Unable to get key for ID %s of user %s.", signer_identity, signer_user);
}
}
JS_FreeCString(context, signer_user);
JS_FreeCString(context, signer_identity);
JS_FreeCString(context, message);
return result; return result;
} }
static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) typedef struct _private_message_decrypt_t
{ {
JSValue result = JS_UNDEFINED; const char* user;
const char* user = JS_ToCString(context, argv[0]); const char* identity;
const char* identity = JS_ToCString(context, argv[1]); size_t message_size;
size_t message_size = 0; const char* message;
const char* message = JS_ToCStringLen(context, &message_size, argv[2]); const char* decrypted;
size_t decrypted_size;
const char* error;
JSValue promise[2];
} private_message_decrypt_t;
static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
{
private_message_decrypt_t* work = user_data;
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(db, user, identity, private_key); bool found = _tf_ssb_get_private_key_curve25519(db, work->user, work->identity, private_key);
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
if (found) if (found)
{ {
uint8_t* decoded = tf_malloc(message_size); uint8_t* decoded = tf_malloc(work->message_size);
int decoded_length = tf_base64_decode(message, message_size - strlen(".box"), decoded, message_size); int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size);
uint8_t* nonce = decoded; uint8_t* nonce = decoded;
uint8_t* public_key = decoded + crypto_box_NONCEBYTES; uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length) if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
@ -1935,36 +2096,83 @@ static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst
uint8_t* key = out + 1; uint8_t* key = out + 1;
if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1) if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
{ {
result = JS_NewStringLen(context, (const char*)decrypted, body_size - crypto_secretbox_MACBYTES); work->decrypted = (const char*)decrypted;
work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
} }
else else
{ {
result = JS_ThrowInternalError(context, "Received key to open secret box containing message body, but it did not work."); work->error = "Received key to open secret box containing message body, but it did not work.";
} }
tf_free(decrypted);
} }
} }
} }
else else
{ {
result = JS_ThrowInternalError(context, "crypto_scalarmult failed."); work->error = "crypto_scalarmult failed.";
} }
} }
else else
{ {
result = JS_ThrowInternalError(context, "Encrypted message was not long enough to contains its one-time public key."); work->error = "Encrypted message was not long enough to contains its one-time public key.";
} }
tf_free(decoded); tf_free(decoded);
} }
else else
{ {
result = JS_ThrowInternalError(context, "Private key not found for user %s with id %s.", user, identity); work->error = "Private key not found for user.";
} }
}
JS_FreeCString(context, user); static void _tf_ssb_private_message_decrypt_after_work(tf_ssb_t* ssb, int status, void* user_data)
JS_FreeCString(context, identity); {
JS_FreeCString(context, message); private_message_decrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue result = JS_ThrowInternalError(context, "%s", work->error);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else if (work->decrypted)
{
JSValue result = JS_NewStringLen(context, work->decrypted, work->decrypted_size);
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
}
else
{
JSValue result = JS_UNDEFINED;
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->identity);
JS_FreeCString(context, work->message);
tf_free((void*)work->decrypted);
tf_free(work);
}
static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* user = JS_ToCString(context, argv[0]);
const char* identity = JS_ToCString(context, argv[1]);
size_t message_size = 0;
const char* message = JS_ToCStringLen(context, &message_size, argv[2]);
private_message_decrypt_t* work = tf_malloc(sizeof(private_message_decrypt_t));
*work = (private_message_decrypt_t) {
.user = user,
.identity = identity,
.message_size = message_size,
.message = message,
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_private_message_decrypt_work, _tf_ssb_private_message_decrypt_after_work, work);
return result; return result;
} }
@ -2092,7 +2300,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "getActiveIdentity", JS_NewCFunction(context, _tf_ssb_getActiveIdentity, "getActiveIdentity", 3)); JS_SetPropertyStr(context, object, "getActiveIdentity", JS_NewCFunction(context, _tf_ssb_getActiveIdentity, "getActiveIdentity", 3));
JS_SetPropertyStr(context, object, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3)); JS_SetPropertyStr(context, object, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object, "blobGet", JS_NewCFunction(context, _tf_ssb_blobGet, "blobGet", 1)); JS_SetPropertyStr(context, object, "blobGet", JS_NewCFunction(context, _tf_ssb_blobGet, "blobGet", 1));
JS_SetPropertyStr(context, object, "messageContentGet", JS_NewCFunction(context, _tf_ssb_messageContentGet, "messageContentGet", 1));
JS_SetPropertyStr(context, object, "connections", JS_NewCFunction(context, _tf_ssb_connections, "connections", 0)); JS_SetPropertyStr(context, object, "connections", JS_NewCFunction(context, _tf_ssb_connections, "connections", 0));
JS_SetPropertyStr(context, object, "storedConnections", JS_NewCFunction(context, _tf_ssb_storedConnections, "storedConnections", 0)); JS_SetPropertyStr(context, object, "storedConnections", JS_NewCFunction(context, _tf_ssb_storedConnections, "storedConnections", 0));
JS_SetPropertyStr(context, object, "getConnection", JS_NewCFunction(context, _tf_ssb_getConnection, "getConnection", 1)); JS_SetPropertyStr(context, object, "getConnection", JS_NewCFunction(context, _tf_ssb_getConnection, "getConnection", 1));

View File

@ -67,6 +67,40 @@ static void _tf_ssb_rpc_blobs_get_callback(
{ {
} }
typedef struct _blobs_get_work_t
{
int64_t request_number;
char id[k_id_base64_len];
bool found;
uint8_t* blob;
size_t size;
} blobs_get_work_t;
static void _tf_ssb_rpc_blobs_get_work(tf_ssb_connection_t* connection, void* user_data)
{
blobs_get_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
work->found = tf_ssb_db_blob_get(ssb, work->id, &work->blob, &work->size);
}
static void _tf_ssb_rpc_blobs_get_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
{
blobs_get_work_t* work = user_data;
if (work->found)
{
const size_t k_send_max = 8192;
for (size_t offset = 0; offset < work->size; offset += k_send_max)
{
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_binary | k_ssb_rpc_flag_stream, -work->request_number, NULL, work->blob + offset,
offset + k_send_max <= work->size ? k_send_max : (work->size - offset), NULL, NULL, NULL);
}
tf_free(work->blob);
}
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error | k_ssb_rpc_flag_stream, -work->request_number, NULL,
(const uint8_t*)(work->found ? "true" : "false"), strlen(work->found ? "true" : "false"), NULL, NULL, NULL);
tf_free(work);
}
static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{ {
if (flags & k_ssb_rpc_flag_end_error) if (flags & k_ssb_rpc_flag_end_error)
@ -75,11 +109,10 @@ static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags
} }
tf_ssb_connection_add_request(connection, -request_number, "blobs.get", _tf_ssb_rpc_blobs_get_callback, NULL, NULL, NULL); tf_ssb_connection_add_request(connection, -request_number, "blobs.get", _tf_ssb_rpc_blobs_get_callback, NULL, NULL, NULL);
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_connection_get_context(connection); JSContext* context = tf_ssb_connection_get_context(connection);
JSValue ids = JS_GetPropertyStr(context, args, "args"); JSValue ids = JS_GetPropertyStr(context, args, "args");
int length = tf_util_get_length(context, ids); int length = tf_util_get_length(context, ids);
bool success = false;
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
JSValue arg = JS_GetPropertyUint32(context, ids, i); JSValue arg = JS_GetPropertyUint32(context, ids, i);
@ -94,25 +127,18 @@ static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags
id = JS_ToCString(context, key); id = JS_ToCString(context, key);
JS_FreeValue(context, key); JS_FreeValue(context, key);
} }
uint8_t* blob = NULL;
size_t size = 0; blobs_get_work_t* work = tf_malloc(sizeof(blobs_get_work_t));
const size_t k_send_max = 8192; *work = (blobs_get_work_t) {
if (tf_ssb_db_blob_get(ssb, id, &blob, &size)) .request_number = request_number,
{ };
for (size_t offset = 0; offset < size; offset += k_send_max) snprintf(work->id, sizeof(work->id), "%s", id);
{ tf_ssb_connection_run_work(connection, _tf_ssb_rpc_blobs_get_work, _tf_ssb_rpc_blobs_get_after_work, work);
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_binary | k_ssb_rpc_flag_stream, -request_number, NULL, blob + offset,
offset + k_send_max <= size ? k_send_max : (size - offset), NULL, NULL, NULL);
}
success = true;
tf_free(blob);
}
JS_FreeCString(context, id); JS_FreeCString(context, id);
JS_FreeValue(context, arg); JS_FreeValue(context, arg);
} }
JS_FreeValue(context, ids); JS_FreeValue(context, ids);
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error | k_ssb_rpc_flag_stream, -request_number, NULL,
(const uint8_t*)(success ? "true" : "false"), strlen(success ? "true" : "false"), NULL, NULL, NULL);
} }
static void _tf_ssb_rpc_blobs_has(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_blobs_has(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
@ -1259,7 +1285,6 @@ static void _tf_ssb_rpc_delete_blobs_work(tf_ssb_t* ssb, void* user_data)
static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data)
{ {
tf_ssb_unref(ssb);
} }
static void _tf_ssb_rpc_start_delete_callback(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_rpc_start_delete_callback(tf_ssb_t* ssb, void* user_data)

View File

@ -16,6 +16,11 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#if defined(_WIN32)
#define WIFEXITED(x) 1
#define WEXITSTATUS(x) (x)
#endif
void tf_ssb_test_id_conversion(const tf_test_options_t* options) void tf_ssb_test_id_conversion(const tf_test_options_t* options)
{ {
tf_printf("Testing id conversion.\n"); tf_printf("Testing id conversion.\n");
@ -819,3 +824,45 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options)
uv_loop_close(&loop); uv_loop_close(&loop);
} }
#if !TARGET_OS_IPHONE
static void _write_file(const char* path, const char* contents)
{
FILE* file = fopen(path, "w");
if (!file)
{
printf("Unable to write %s: %s.\n", path, strerror(errno));
fflush(stdout);
abort();
}
fputs(contents, file);
fclose(file);
}
#define TEST_ARGS " --ssb-port=0 --http-port=0 --https-port=0"
void tf_ssb_test_encrypt(const tf_test_options_t* options)
{
_write_file("out/test.js",
"async function main() {\n"
" let a = await ssb.createIdentity('test');\n"
" let b = await ssb.createIdentity('test');\n"
" let c = await ssb.privateMessageEncrypt('test', a, [a, b], \"{'foo': 1}\");\n"
" if (!c.endsWith('.box')) {\n"
" exit(1);\n"
" }\n"
" print(await ssb.privateMessageDecrypt('test', a, c));\n"
"}\n"
"main().catch(() => exit(2));\n");
unlink("out/testdb.sqlite");
char command[256];
snprintf(command, sizeof(command), "%s run --db-path=out/testdb.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command);
int result = system(command);
(void)result;
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
}
#endif

View File

@ -47,4 +47,10 @@ void tf_ssb_test_bench(const tf_test_options_t* options);
*/ */
void tf_ssb_test_go_ssb_room(const tf_test_options_t* options); void tf_ssb_test_go_ssb_room(const tf_test_options_t* options);
/**
** Test encrypting a private message.
** @param options The test options.
*/
void tf_ssb_test_encrypt(const tf_test_options_t* options);
/** @} */ /** @} */

View File

@ -275,8 +275,17 @@ static void _test_database(const tf_test_options_t* options)
" if (await db.get('a') != 1) {\n" " if (await db.get('a') != 1) {\n"
" exit(2);\n" " exit(2);\n"
" }\n" " }\n"
" await db.set('b', 2);\n" " await db.exchange('b', null, 1);\n"
" await db.exchange('b', 1, 2);\n"
" if (await db.get('b') != 2) {\n"
" exit(5);\n"
" }\n"
" await db.set('c', 3);\n" " await db.set('c', 3);\n"
" await db.set('d', 3);\n"
" await db.remove('d', 3);\n"
" if (JSON.stringify(await db.getLike('b%')) != '{\"b\":\"2\"}') {\n"
" exit(6);\n"
" }\n"
"\n" "\n"
" var expected = ['a', 'b', 'c'];\n" " var expected = ['a', 'b', 'c'];\n"
" var have = await db.getAll();\n" " var have = await db.getAll();\n"
@ -293,6 +302,9 @@ static void _test_database(const tf_test_options_t* options)
" print('Expected but did not find: ' + JSON.stringify(expected));\n" " print('Expected but did not find: ' + JSON.stringify(expected));\n"
" exit(4);\n" " exit(4);\n"
" }\n" " }\n"
" if (JSON.stringify(await databases.list('%')) != '[\"testdb\"]') {\n"
" exit(7);\n"
" }\n"
"}\n" "}\n"
"main();"); "main();");
@ -902,6 +914,7 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "bench", tf_ssb_test_bench, false); _tf_test_run(options, "bench", tf_ssb_test_bench, false);
_tf_test_run(options, "auto", _test_auto, false); _tf_test_run(options, "auto", _test_auto, false);
_tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true); _tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true);
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
tf_printf("Tests completed.\n"); tf_printf("Tests completed.\n");
#endif #endif
} }

View File

@ -435,6 +435,16 @@ const char* tf_util_backtrace_string()
return tf_util_backtrace_to_string(buffer, count); return tf_util_backtrace_to_string(buffer, count);
} }
void tf_util_print_backtrace()
{
const char* bt = tf_util_backtrace_string();
if (bt)
{
tf_printf("%s\n", bt);
}
tf_free((void*)bt);
}
#if defined(__ANDROID__) #if defined(__ANDROID__)
typedef struct _android_backtrace_t typedef struct _android_backtrace_t
{ {

View File

@ -126,6 +126,11 @@ const char* tf_util_backtrace_to_string(void* const* buffer, int count);
*/ */
const char* tf_util_backtrace_string(); const char* tf_util_backtrace_string();
/**
** Print a stack backtrace of the calling thread.
*/
void tf_util_print_backtrace();
/** /**
** Convert a function pointer to its name, if possible. ** Convert a function pointer to its name, if possible.
** @return The function name or null. ** @return The function name or null.

View File

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.20-wip" #define VERSION_NUMBER "0.0.20"
#define VERSION_NAME "One word all lowercase four words all uppercase." #define VERSION_NAME "They gave you a small cup."

View File

@ -125,7 +125,8 @@ try:
driver.switch_to.default_content() driver.switch_to.default_content()
driver.find_element(By.ID, 'allow').click() driver.find_element(By.ID, 'allow').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click() driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
@ -133,13 +134,8 @@ try:
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
# NoSuchElementException driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
while True: driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
try:
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click()
break
except:
pass
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
@ -148,7 +144,7 @@ try:
wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
driver.switch_to.default_content() driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout guest').click() driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
@ -189,7 +185,8 @@ try:
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'logout testuser').click() driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password') driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')