34 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
cb3c7afade Move ssb.getPrivateKey's DB work off the main thread. 2024-06-16 08:07:02 -04:00
991022adfc Move ssb.appendMessageWithIdentity's DB work off the main thread. 2024-06-16 07:51:06 -04:00
2bc71a18a6 Make ssb.deleteIdentity not block the main thread with DB work. 2024-06-14 17:39:24 -04:00
57ca864fbb Build fix. 2024-06-12 21:08:41 -04:00
a09edfb612 ssb.addIdentity without hitting the DB from the main thread. 2024-06-12 21:06:30 -04:00
29 changed files with 1155 additions and 556 deletions

View File

@ -4,8 +4,8 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 20
VERSION_NUMBER := 0.0.20-wip
VERSION_NAME := One word all lowercase four words all uppercase.
VERSION_NUMBER := 0.0.20
VERSION_NAME := They gave you a small cup.
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

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"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');
preview.innerHTML = this.process_text(edit.innerText);
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();
draft.text = edit.innerText;
draft.content_warning = content_warning?.innerText;
draft.content_warning = content_warning?.value;
setTimeout(() => this.notify(draft), 0);
}
@ -221,12 +215,8 @@ class TfComposeElement extends LitElement {
console.log('encrypted as', message);
}
try {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
edit.innerText = '';
self.input();
self.notify(undefined);
self.requestUpdate();
});
await tfrpc.rpc.appendMessage(this.whoami, message);
self.notify(undefined);
} catch (error) {
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>
<label for="cw">CW</label>
</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>
`;
} else {

View File

@ -3,7 +3,7 @@
<head>
<title>Tilde Friends Sign-in</title>
<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" />
</head>
<body>

View File

@ -118,28 +118,6 @@ class TfNavigationElement extends LitElement {
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) {
send({action: 'setActiveIdentity', identity: id});
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
@ -159,66 +137,105 @@ class TfNavigationElement extends LitElement {
window.location.href = '/~core/ssb/#' + this.identity;
}
logout() {
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
}
render_identity() {
let self = this;
if (this.identities?.length) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
<button
class="w3-button w3-rest w3-cyan"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
@click=${self.toggle_id_dropdown}
>
${self.names[this.identity]}
</button>
<div
id="id_dropdown"
class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%; right: 0"
>
if (this?.credentials?.session?.name) {
if (this.identities?.length) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
<button
class="w3-bar-item w3-button w3-border"
@click=${() => (window.location.href = '/~core/identity')}
class="w3-button w3-rest w3-cyan"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
id="identity"
@click=${self.toggle_id_dropdown}
>
Manage Identities...
${self.names[this.identity]}
</button>
<button
class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile}
<div
id="id_dropdown"
class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%; right: 0"
>
Edit Profile...
</button>
${this.identities.map(
(x) => html`
<button
class="w3-bar-item w3-button ${x === self.identity
? 'w3-cyan'
: ''}"
@click=${() => self.set_active_identity(x)}
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
>
${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
</button>
`
)}
<button
class="w3-bar-item w3-button w3-border"
@click=${() => (window.location.href = '/~core/identity')}
>
Manage Identities...
</button>
<button
class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile}
>
Edit Profile...
</button>
${this.identities.map(
(x) => html`
<button
class="w3-bar-item w3-button ${x === self.identity
? 'w3-cyan'
: ''}"
@click=${() => self.set_active_identity(x)}
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
>
${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
</button>
`
)}
<button
class="w3-bar-item w3-button w3-border"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
</div>
</div>
</div>
`;
} else if (
this.credentials?.session?.name &&
this.credentials.session.name !== 'guest'
) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<button
id="create_identity"
@click=${this.create_identity}
class="w3-button w3-mobile w3-blue w3-right"
>
Create an Identity
</button>
`;
`;
} else if (
this.credentials?.session?.name &&
this.credentials.session.name !== 'guest'
) {
return html`
<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
id="create_identity"
@click=${this.create_identity}
class="w3-button w3-mobile w3-red w3-right"
>
Create an Identity
</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)
.sort()
.map((x) => this.spark_lines[x])}
${this.render_login()} ${this.render_identity()}
${this.render_identity()}
</div>
${this.status?.is_error
? html`

View File

@ -512,7 +512,7 @@ async function getProcessBlob(blobId, key, options) {
print('Done.');
};
imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user')
await imports.core.permissionTest('delete_user');
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
@ -749,7 +749,7 @@ async function getProcessBlob(blobId, key, options) {
};
process.task.setImports(imports);
process.task.activate();
let source = await getBlobOrContent(blobId);
let source = await ssb.blobGet(blobId);
let appSourceName = blobId;
let appSource = utf8Decode(source);
try {
@ -757,7 +757,7 @@ async function getProcessBlob(blobId, key, options) {
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await getBlobOrContent(id);
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
@ -767,7 +767,7 @@ async function getProcessBlob(blobId, key, options) {
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
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;
/**
@ -993,7 +978,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(id);
data = await ssb.blobGet(id);
if (match[3]) {
let appObject = JSON.parse(data);
data = appObject.files[match[3]];
@ -1025,7 +1010,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(blobId);
data = await ssb.blobGet(blobId);
sendData(
response,
data,
@ -1141,7 +1126,7 @@ async function blobHandler(request, response, blobId, uri) {
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)];
if (!id && app_object?.files['handler.js']) {
let answer;
@ -1197,7 +1182,7 @@ async function blobHandler(request, response, blobId, uri) {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
};
data = await getBlobOrContent(id);
data = await ssb.blobGet(id);
let type =
httpd.mime_type_from_extension(uri) ||
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>
<link type="text/css" rel="stylesheet" href="/static/style.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" />
<script>
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 {
pname = "tildefriends";
version = "0.0.19";
version = "0.0.20";
src = pkgs.fetchFromGitea {
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": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz",
"integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==",
"version": "6.16.2",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz",
"integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@ -36,13 +36,13 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz",
"integrity": "sha512-rK+sj4fCAN/QfcY9BEzYMgp4wwL/q5aj/VfNSoH1RWPF9XS/dUwBkvlL3hpWgEjOqlpdN1uLC9UkjJ4tmyjJYg==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
"integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.0.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
@ -98,9 +98,9 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@ -147,9 +147,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.26.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz",
"integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==",
"version": "6.28.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.1.tgz",
"integrity": "sha512-BUWr+zCJpMkA/u69HlJmR+YkV4yPpM81HeMkOMZuwFa8iM5uJdEPKAs1icIRZKkKmy0Ub1x9/G3PQLTXdpBxrQ==",
"dependencies": {
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
@ -238,9 +238,9 @@
}
},
"node_modules/@lezer/html": {
"version": "1.3.9",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.9.tgz",
"integrity": "sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==",
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -248,9 +248,9 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.16",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.16.tgz",
"integrity": "sha512-84UXR3N7s11MPQHWgMnjb9571fr19MmXnr5zTv2XX0gHXXUvW3uPJ8GCjKrfTXmSdfktjRK0ayKklw+A13rk4g==",
"version": "1.4.17",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz",
"integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@ -268,9 +268,9 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz",
"integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@ -545,9 +545,9 @@
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@ -819,9 +819,9 @@
}
},
"node_modules/terser": {
"version": "5.31.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz",
"integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==",
"version": "5.31.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz",
"integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",

View File

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

View File

@ -51,7 +51,7 @@ void tf_database_register(JSContext* context)
JS_SetPropertyStr(context, global, "Database", constructor);
JSValue databases = JS_NewObject(context);
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);
}
@ -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_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;
}
@ -245,79 +245,146 @@ static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc
return result;
}
typedef struct _database_exchange_t
{
const char* id;
const char* key;
size_t key_length;
const char* expected;
size_t expected_length;
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_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_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_DONE)
{
work->result = sqlite3_changes(db) != 0;
}
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)
{
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 &&
sqlite3_bind_text(statement, 3, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 4, work->expected, work->expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{
work->result = sqlite3_changes(db) != 0;
}
sqlite3_finalize(statement);
}
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 exchanged = JS_UNDEFINED;
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1]))
{
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;
size_t set_length;
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;
}
JS_FreeCString(context, key);
JS_FreeCString(context, set);
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)
{
size_t key_length;
size_t expected_length;
size_t set_length;
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;
}
JS_FreeCString(context, key);
JS_FreeCString(context, expected);
JS_FreeCString(context, set);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
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)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
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)
{
size_t keyLength;
const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]);
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)
{
}
JS_FreeCString(context, keyString);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
size_t key_length = 0;
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
database_remove_t* work = tf_malloc(sizeof(database_remove_t) + key_length + 1);
*work = (database_remove_t) {
.id = tf_strdup(database->id),
.key_length = key_length,
};
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);
}
return JS_UNDEFINED;
return result;
}
typedef struct _database_get_all_t
@ -406,57 +473,153 @@ static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int
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)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
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)
{
const char* pattern = JS_ToCString(context, argv[0]);
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)
{
JS_SetPropertyStr(context, result, (const char*)sqlite3_column_text(statement, 0),
JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 1), sqlite3_column_bytes(statement, 1)));
}
}
JS_FreeCString(context, pattern);
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
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_stmt* statement;
if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->pattern, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
work->names = tf_resize_vec(work->names, sizeof(char*) * (work->names_length + 1));
work->names[work->names_length] = tf_strdup((const char*)sqlite3_column_text(statement, 0));
work->names_length++;
}
}
sqlite3_finalize(statement);
}
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]);
}
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)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
JSValue array = JS_UNDEFINED;
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, pattern, -1, NULL) == SQLITE_OK)
{
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;
databases_list_t* work = tf_malloc(sizeof(databases_list_t));
*work = (databases_list_t) {
.pattern = JS_ToCString(context, argv[0]),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _databases_list_work, _databases_list_after_work, work);
return result;
}

View File

@ -1019,7 +1019,7 @@ void tf_http_request_unref(tf_http_request_t* request)
tf_free(request);
}
if (--connection->ref_count == 0)
if (connection && --connection->ref_count == 0)
{
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[] = {
"index.html",
"client.js",
"favicon.png",
"tildefriends.svg",
"jszip.min.js",
"style.css",
"tfrpc.js",
@ -1351,11 +1351,10 @@ static bool _verify_password(const char* password, const char* hash)
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");
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))
{
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);
}
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)
{
@ -1541,7 +1540,6 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
}
else
{
tf_http_request_ref(request);
login->name = account_name_copy;
login->error = login_error;
@ -1566,6 +1564,7 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
}
login->pending++;
tf_http_request_ref(request);
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
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)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (login->pending == 1)
{
tf_http_request_t* request = login->request;
if (*login->location_header)
{
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_request_unref(request);
}
tf_http_request_unref(request);
_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);
}
}
else
{
tf_printf("No request callback for %p %d\n", connection, -request_number);
}
}
if (close_connection)
@ -1784,18 +1780,28 @@ static bool _tf_ssb_connection_box_stream_recv(tf_ssb_connection_t* connection)
return true;
}
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message)
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int64_t previous_sequence)
{
char previous_id[crypto_hash_sha256_BYTES * 2];
int64_t previous_sequence = 0;
bool have_previous = tf_ssb_db_get_latest_message_by_author(ssb, author, &previous_sequence, previous_id, sizeof(previous_id));
char actual_previous_id[crypto_hash_sha256_BYTES * 2];
int64_t actual_previous_sequence = 0;
bool have_previous = false;
if (previous_id)
{
have_previous = *previous_id && previous_sequence > 0;
snprintf(actual_previous_id, sizeof(actual_previous_id), "%s", previous_id);
actual_previous_sequence = previous_sequence;
}
else
{
have_previous = tf_ssb_db_get_latest_message_by_author(ssb, author, &actual_previous_sequence, actual_previous_id, sizeof(actual_previous_id));
}
JSContext* context = ssb->context;
JSValue root = JS_NewObject(context);
JS_SetPropertyStr(context, root, "previous", have_previous ? JS_NewString(context, previous_id) : JS_NULL);
JS_SetPropertyStr(context, root, "previous", have_previous ? JS_NewString(context, actual_previous_id) : JS_NULL);
JS_SetPropertyStr(context, root, "author", JS_NewString(context, author));
JS_SetPropertyStr(context, root, "sequence", JS_NewInt64(context, previous_sequence + 1));
JS_SetPropertyStr(context, root, "sequence", JS_NewInt64(context, actual_previous_sequence + 1));
int64_t now = (int64_t)time(NULL);
JS_SetPropertyStr(context, root, "timestamp", JS_NewInt64(context, now * 1000LL));
@ -2260,6 +2266,7 @@ static void _tf_ssb_assert_not_main_thread(tf_ssb_t* ssb)
const char* bt = tf_util_backtrace_string();
tf_printf("Acquiring DB from the main thread:\n%s\n", bt);
tf_free((void*)bt);
abort();
}
}
@ -3749,7 +3756,12 @@ void tf_ssb_ref(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)

View File

@ -281,7 +281,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
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;
if (sequence == 1)
@ -291,12 +291,13 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
else
{
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 &&
sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
exists = sqlite3_column_int(statement, 0) != 0;
*out_id_mismatch = sqlite3_column_int(statement, 1) != 0;
}
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);
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(?), "
"?, ?, ?) 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));
}
}
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_ssb_release_db_writer(ssb, db);

View File

@ -278,9 +278,11 @@ void tf_ssb_run(tf_ssb_t* ssb);
** @param author The author's public key.
** @param private_key The author's private key.
** @param message The message to sign.
** @param previous_id The ID of the previous message in the feed. Optional.
** @param previous_sequence The sequence number of the previous message in the feed. Optional.
** @return The signed message.
*/
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message);
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int64_t previous_sequence);
/**
** Get the server's identity.

File diff suppressed because it is too large Load Diff

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)
{
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_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue ids = JS_GetPropertyStr(context, args, "args");
int length = tf_util_get_length(context, ids);
bool success = false;
for (int i = 0; i < length; 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);
JS_FreeValue(context, key);
}
uint8_t* blob = NULL;
size_t size = 0;
const size_t k_send_max = 8192;
if (tf_ssb_db_blob_get(ssb, id, &blob, &size))
{
for (size_t offset = 0; offset < size; offset += k_send_max)
{
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);
}
blobs_get_work_t* work = tf_malloc(sizeof(blobs_get_work_t));
*work = (blobs_get_work_t) {
.request_number = request_number,
};
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);
JS_FreeCString(context, id);
JS_FreeValue(context, arg);
}
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)
@ -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)
{
tf_ssb_unref(ssb);
}
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 <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)
{
tf_printf("Testing id conversion.\n");
@ -192,7 +197,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "Hello, world!"));
bool stored = false;
JSValue signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj);
JSValue signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
@ -202,7 +207,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "First post."));
stored = false;
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj);
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
@ -217,7 +222,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_SetPropertyUint32(context0, mentions, 0, mention);
JS_SetPropertyStr(context0, obj, "mentions", mentions);
stored = false;
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj);
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
@ -276,7 +281,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "Message to self."));
stored = false;
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj);
signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
@ -549,7 +554,7 @@ void tf_ssb_test_following(const tf_test_options_t* options)
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, contact)); \
JS_SetPropertyStr(context, message, "following", follow ? JS_TRUE : JS_FALSE); \
JS_SetPropertyStr(context, message, "blocking", block ? JS_TRUE : JS_FALSE); \
signed_message = tf_ssb_sign_message(ssb0, id, priv, message); \
signed_message = tf_ssb_sign_message(ssb0, id, priv, message, NULL, 0); \
stored = false; \
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored); \
_wait_stored(ssb0, &stored); \
@ -608,7 +613,7 @@ void tf_ssb_test_bench(const tf_test_options_t* options)
for (int i = 0; i < k_messages; i++)
{
bool stored = false;
JSValue signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj);
JSValue signed_message = tf_ssb_sign_message(ssb0, id0, priv0, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(tf_ssb_get_context(ssb0), signed_message);
_wait_stored(ssb0, &stored);
@ -819,3 +824,45 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options)
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);
/**
** 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"
" exit(2);\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('d', 3);\n"
" await db.remove('d', 3);\n"
" if (JSON.stringify(await db.getLike('b%')) != '{\"b\":\"2\"}') {\n"
" exit(6);\n"
" }\n"
"\n"
" var expected = ['a', 'b', 'c'];\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"
" exit(4);\n"
" }\n"
" if (JSON.stringify(await databases.list('%')) != '[\"testdb\"]') {\n"
" exit(7);\n"
" }\n"
"}\n"
"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, "auto", _test_auto, false);
_tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true);
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
tf_printf("Tests completed.\n");
#endif
}

View File

@ -435,6 +435,16 @@ const char* tf_util_backtrace_string()
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__)
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();
/**
** Print a stack backtrace of the calling thread.
*/
void tf_util_print_backtrace();
/**
** Convert a function pointer to its name, if possible.
** @return The function name or null.

View File

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

View File

@ -125,7 +125,8 @@ try:
driver.switch_to.default_content()
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, 'name').send_keys('testuser')
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')))
# NoSuchElementException
while True:
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-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, 'guest_label').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
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, '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, 'loginButton').click()
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, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')