21 Commits

Author SHA1 Message Date
85bbb9c010 docs: Update build notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 10m13s
2025-12-27 16:30:09 -05:00
82b0fe8d57 build: Actually dist the aarch64 appimage.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-27 16:27:08 -05:00
00b233dd95 core: A little more tracing. 2025-12-27 16:14:30 -05:00
7154212ddd ssb: Make editing your profile look a little more obvious.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 11m27s
2025-12-27 15:55:16 -05:00
364c4c04ac core: loadSettings() no longer needed.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m23s
Build Tilde Friends / Build-All (push) Successful in 10m56s
2025-12-27 14:51:33 -05:00
878c022934 ssb: Attempt to group related messages (bookclub + bookclubUpdates, wiki + wiki-doc, ...) 2025-12-27 14:35:58 -05:00
2c9654b480 android: Fix copying message ids based on: https://stackoverflow.com/questions/61401384/can-text-within-an-iframe-be-copied-to-clipboard.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 12m21s
2025-12-27 11:55:29 -05:00
14e36308f9 ssb: Fix the messages_stats trigger.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m41s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-27 11:24:09 -05:00
cd8df2fe15 welcome: Routine re-wordsmithing.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m47s
Build Tilde Friends / Build-All (push) Successful in 10m36s
2025-12-27 11:04:48 -05:00
8abcdd1e7d core: Fix unauthenticated sessions.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m38s
Build Tilde Friends / Build-All (push) Successful in 9m52s
2025-12-26 21:53:54 -05:00
97aeff60cc build: Build an aarch64 .AppImage.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m28s
Build Tilde Friends / Build-All (push) Successful in 10m6s
2025-12-26 21:40:11 -05:00
86d6a5c049 update: CodeMirror. 2025-12-26 21:24:52 -05:00
f6f815eec1 ssb: Fix the oblong spinning refresh button. 2025-12-26 21:22:00 -05:00
73a1c1d978 core: Move ssb.getPrivateKey from JS => C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-12-26 17:27:36 -05:00
5445072d36 core: Move ssb.deleteIdentity from JS => C. 2025-12-26 17:07:07 -05:00
d9a2519e9b core: Move ssb.addItentity() from JS => C. 2025-12-26 16:48:46 -05:00
687a85dbd8 build: Disable -t auto again. Oh well.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Successful in 10m26s
2025-12-26 16:16:33 -05:00
9e25aa1c54 build: Maybe on trixie?
Some checks failed
Build Tilde Friends / Build-Docs (push) Failing after 4m41s
Build Tilde Friends / Build-All (push) Successful in 11m3s
2025-12-26 10:13:08 -05:00
e309f519f2 build: Oops, actually test the thing.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Failing after 10m55s
2025-12-25 11:18:46 -05:00
5ccd9f16c3 build: Last try.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-25 11:14:10 -05:00
7d596ebd3b build: Maybe like this?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Failing after 6s
2025-12-25 11:10:01 -05:00
21 changed files with 480 additions and 366 deletions

View File

@@ -54,13 +54,11 @@ jobs:
docker.io \
doxygen \
file \
firefox-geckodriver \
gcc-aarch64-linux-gnu \
git \
graphviz \
libgpgme11 \
libssl-dev \
python3-selenium \
mingw-w64 \
rsync \
unzip \

View File

@@ -1213,7 +1213,26 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
out/tildefriends-aarch64.AppImage: out/armrelease/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends_aarch64.AppDir
@mkdir -p out/tildefriends_aarch64.AppDir/usr/bin
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/applications
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends_aarch64.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/
@cp out/armrelease/tildefriends out/tildefriends_aarch64.AppDir/usr/bin/
@cp out/data.zip out/tildefriends_aarch64.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends_aarch64.AppDir/AppRun
@chmod +x out/tildefriends_aarch64.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=arm_aarch64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-aarch64.AppImage.zsync' tildefriends_aarch64.AppDir tildefriends-aarch64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage out/tildefriends-aarch64.AppImage ## Build AppImages.
.PHONY: appimage
flatpak: out/ ## Build a flatpak.
@@ -1300,6 +1319,8 @@ dist:
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
@echo "[cp] TildeFriends-aarch64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-aarch64.AppImage dist/TildeFriends-aarch64-$(VERSION_NUMBER).AppImage
@echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)
@test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&S0BuSm19vBzA6Gf97/rYcwndKyb55MxoITnz7xRjlzg=.sha256"
"previous": "&PrgfYYKpL2tedApJXokIIk+h9/yRYo0aUliNF3QsnZ4=.sha256"
}

View File

@@ -866,16 +866,20 @@ class TfElement extends LitElement {
this.is_administrator
? html`
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple w3-right' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
class="w3-bar-item w3-button w3-circle w3-right"
@click=${this.refresh}
>
<span
style="display: inline-block"
class=${this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: ''}
>
</span>
</button>
<button
class="w3-bar-item w3-button w3-ripple w3-right"
class="w3-bar-item w3-button w3-right"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}

View File

@@ -2,6 +2,7 @@ import {
LitElement,
css,
html,
map,
repeat,
render,
unsafeCSS,
@@ -472,7 +473,9 @@ class TfMessageElement extends LitElement {
}
copy_id(event) {
navigator.clipboard.writeText(this.message?.id);
navigator.clipboard.writeText(this.message?.id).catch(function (e) {
console.log(e);
});
}
toggle_menu(event) {
@@ -595,6 +598,31 @@ class TfMessageElement extends LitElement {
`;
}
render_refs() {
if (this.message?.refs) {
let self = this;
return html`<div class="w3-container">
<h3>Referring messages</h3>
${map(
this.message.refs,
(ref) => html`
<tf-message
.message=${ref}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
.recent_reactions=${self.recent_reactions}
depth=${self.depth + 1}
></tf-message>
`
)}
</div>`;
}
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
@@ -604,7 +632,7 @@ class TfMessageElement extends LitElement {
? html`${self.render_raw()}`
: self.render_flagged(inner)}
</div>
${self.render_votes()}
${self.render_votes()} ${self.render_refs()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -914,6 +942,7 @@ class TfMessageElement extends LitElement {
</div>
<div class="w3-container">${this.render_flagged(undefined)}</div>
<div>${this.render_votes()}</div>
${this.render_refs()}
${(this.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -980,7 +1009,7 @@ class TfMessageElement extends LitElement {
></tf-user>
</div>
${this.render_menu()} ${this.render_votes()}
${this.render_actions()}
${this.render_refs()} ${this.render_actions()}
</div>
`);
break;
@@ -988,7 +1017,9 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
break;
@@ -1051,12 +1082,15 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${payload}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
} else if (content.type === 'issue') {
return this.render_frame(html`
${this.render_header()} ${content.text} ${this.render_votes()}
${this.render_refs()}
<footer class="w3-container">
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
@@ -1109,7 +1143,7 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div>${body}</div>
${this.render_mentions()} ${this.render_votes()}
${this.render_mentions()} ${this.render_votes()} ${this.render_refs()}
${this.render_actions()}
`);
} else if (content.type === 'pub') {
@@ -1160,9 +1194,7 @@ class TfMessageElement extends LitElement {
}
} else {
return this.render_small_frame(
html`<div class="w3-container">
<p><b>type</b>: ${content.type}</p>
</div>`
html`<p><b>type</b>: ${content.type}</p>`
);
}
} else if (typeof this.message.content == 'string') {

View File

@@ -95,6 +95,22 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.root[0];
}
}
} else {
let parent_id = message.content.about?.startswith?.('%')
? message.content.about
: message.content.updates?.startsWith?.('%')
? message.content.updates
: message.content.parent?.startsWith?.('%')
? message.content.parent
: undefined;
if (parent_id) {
let parent = ensure_message(parent_id, message.rowid);
if (!parent?.refs) {
parent.refs = [];
}
parent.refs.push(message);
message.parent_message = parent_id;
}
}
}

View File

@@ -274,16 +274,18 @@ class TfProfileElement extends LitElement {
if (this.id === this.whoami) {
if (this.editing) {
edit = html`
<button
id="save_profile"
class="w3-button w3-theme-d1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
<div style="margin-top: 8px">
<button
id="save_profile"
class="w3-button w3-theme-l1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
</div>
`;
} else {
edit = html`<button
@@ -329,7 +331,7 @@ class TfProfileElement extends LitElement {
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>`
</div>`
: null;
let image = profile.image;
if (typeof image == 'string' && !image.startsWith('&')) {
@@ -341,7 +343,7 @@ class TfProfileElement extends LitElement {
let description = this.editing?.description ?? profile.description;
return html`
<style>${generate_theme()}</style>
<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<div class="w3-card-4 w3-theme-d3" style="box-sizing: border-box">
<header class="w3-container">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
</header>
@@ -350,19 +352,23 @@ class TfProfileElement extends LitElement {
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
<div class=${this.editing ? 'w3-card' : ''}>
${this.editing ? html`<header class="w3-container w3-theme-l2"><h2>Editing Your Profile</h2></header>` : undefined}
<div style="display: flex; flex-direction: row; gap: 1em" class="w3-margin">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: min(256px, 100%); height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
${this.editing ? html`<footer class="w3-container w3-theme-l2"><p>${edit}</p></footer>` : undefined}
</div>
<div>
Following ${profile.following} identities.
@@ -377,7 +383,7 @@ class TfProfileElement extends LitElement {
<button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat">
Open Private Chat
</button>
${edit}
${this.editing ? undefined : edit}
${follow}
${block}
</p>

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&IwbeqN5jcUWa8wbnWxduiwel9VldRO/dARDjljX33PM=.sha256"
"previous": "&sVSmI40DUgnS4TUa2AiKrlNj+qN3WDeXII3364OSMIo=.sha256"
}

View File

@@ -268,11 +268,11 @@
</div>
<!-- Sandbox Section -->
<div class="w3-padding-64 w3-grey">
<div class="w3-padding-64 w3-pale-blue">
<div class="w3-row-padding">
<div class="w3-col">
<h1 class="w3-jumbo" style="text-align: right">
<b>Sandbox Security</b>
<b>App Sandboxes</b>
</h1>
<i
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
@@ -280,7 +280,7 @@
></i>
<p>
Tilde Friends tries to make sure apps can be trusted using similar
techniques to how web browsers and operating systems do it.
techniques to web browsers and operating systems.
</p>
<p>
This is all a work in progress, and it varies by platform, so don't
@@ -294,16 +294,24 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Built to Last</b></h1>
<p>
Tilde Friends strives to use only simple and widely adopted dependencies
in order to keep it easy to build for all sorts of platforms and
maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits on top.
</p>
<h1 class="w3-jumbo"><b>One Pile of Code</b></h1>
<div class="w3-left-align">
<p>
Tilde Friends diverges from the Node.js web of modules from which
Secure Scuttlebutt was first developed. Here we strive to maintain a
single C program that works as a foundation for building a wide
variety of social and other applications.
</p>
<p>
Tilde Friends uses only a handful of simple and widely adopted
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever works.
</p>
</div>
<div class="w3-row" style="margin-top: 64px">
<a
@@ -373,7 +381,7 @@
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
<p class="w3-medium">
This page and Tilde Friends itself was made by Cory mostly in coffee
shops and a local pizza place.
shops and a local pizza place while listening to Gary's Bangers.
</p>
</footer>
</body>

View File

@@ -1037,6 +1037,7 @@ async function update_html() {
let new_html = new_doc.getElementById('document').attributes.srcdoc.value;
let iframe = document.getElementById('document');
let sandbox = iframe.sandbox;
let allow = iframe.allow;
let iframe_parent = iframe.parentNode;
iframe_parent.removeChild(iframe);
@@ -1045,6 +1046,7 @@ async function update_html() {
new_iframe.sandbox = sandbox;
new_iframe.id = 'document';
new_iframe.srcdoc = new_html;
new_iframe.allow = allow;
iframe_parent.appendChild(new_iframe);
} catch (e) {
alert(error);

View File

@@ -231,25 +231,9 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
user: getUser(process, process),
permissionTest: async function (permission, description) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (!user || !options?.packageOwner || !options?.packageName) {
return;
} else if (
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission] !== undefined
) {
if (
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission]
) {
let permissions = await imports.core.permissionsGranted();
if (permissions && permissions[permission] !== undefined) {
if (permissions[permission]) {
return true;
} else {
throw Error(`Permission denied: ${permission}.`);
@@ -375,46 +359,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
);
imports.ssb.createIdentity = () => process.createIdentity();
imports.ssb.addIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_add')
).then(function () {
return ssb.addIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.deleteIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_delete')
).then(function () {
return ssb.deleteIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getPrivateKey = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_export')
).then(function () {
return ssb.getPrivateKey(process.credentials.session.name, id);
});
}
};
if (
process.credentials &&
process.credentials.session &&
@@ -572,28 +517,6 @@ ssb_internal.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []);
});
/**
* Load settings from the database.
* @return The settings as a key value pairs object.
*/
async function loadSettings() {
let data = {};
try {
let settings = await new Database('core').get('settings');
if (settings) {
data = JSON.parse(settings);
}
} catch (error) {
print('Settings not found in database:', error);
}
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
}
return data;
}
/**
* Send periodic stats to all clients.
*/

View File

@@ -178,6 +178,7 @@
<iframe
id="document"
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
allow="clipboard-write"
></iframe>
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -186,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.39.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.6.tgz",
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==",
"version": "6.39.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.7.tgz",
"integrity": "sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",

View File

@@ -1,6 +1,7 @@
* First launch after update may be noticeably slower due to database reindexing.
* Crash fixes.
* Fix channels with hyphens and various other characters not working correctly.
* Navigation bar and search UI improvements.
* Navigation bar, search UI, private message, and profile improvements.
* Faster loads, though the first launch may be particularly slow as indexes are rebuilt.
* Fixed various broken links.
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1.

View File

@@ -5,6 +5,7 @@
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "trace.h"
#include "util.js.h"
#include "quickjs.h"
@@ -890,13 +891,18 @@ typedef struct _permission_test_t
bool completed;
permission_test_callback_t* callback;
void* user_data;
char name[];
} permission_test_t;
static JSValue _tf_ssb_permission_test_resolve(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true;
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(context, true, argv[0], work->user_data);
tf_trace_end(trace);
return JS_UNDEFINED;
}
@@ -904,7 +910,11 @@ static JSValue _tf_ssb_permission_test_reject(JSContext* context, JSValueConst t
{
permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true;
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(context, false, argv[0], work->user_data);
tf_trace_end(trace);
return JS_UNDEFINED;
}
@@ -914,7 +924,11 @@ static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
if (!work->completed)
{
JSValue arg = JS_ThrowInternalError(work->context, "Permission test incomplete.");
tf_task_t* task = tf_task_get(work->context);
tf_trace_t* trace = tf_task_get_trace(task);
tf_trace_begin(trace, work->name);
work->callback(work->context, false, arg, work->user_data);
tf_trace_end(trace);
JS_FreeValue(work->context, arg);
}
tf_free(work);
@@ -932,12 +946,16 @@ static void _tf_ssb_permission_test(JSContext* context, JSValue process, const c
JS_NewClass(JS_GetRuntime(context), s_permission_test_class_id, &def);
}
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
const char* name = tf_util_function_to_string(callback);
size_t name_length = name ? strlen(name) : 0;
permission_test_t* payload = tf_malloc(sizeof(permission_test_t) + name_length + 1);
*payload = (permission_test_t) {
.context = context,
.callback = callback,
.user_data = user_data,
};
tf_string_set(payload->name, name_length + 1, name);
tf_free((void*)name);
JSValue opaque = JS_NewObjectClass(context, s_permission_test_class_id);
JS_SetOpaque(opaque, payload);
JSValue imports = JS_GetPropertyStr(context, process, "imports");
@@ -1728,6 +1746,261 @@ static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueCons
}
}
typedef struct _add_identity_t
{
uint8_t key[crypto_sign_SECRETKEYBYTES / 2];
bool added;
JSValue result;
JSValue promise[2];
char user[];
} add_identity_t;
static void _tf_ssb_add_identity_work(tf_ssb_t* ssb, void* user_data)
{
add_identity_t* work = user_data;
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
unsigned char seed[crypto_sign_SEEDBYTES];
uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
memcpy(secret_key, work->key, sizeof(secret_key) / 2);
if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
{
char public_key_b64[512];
tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
snprintf(public_key_b64 + strlen(public_key_b64), sizeof(public_key_b64) - strlen(public_key_b64), ".ed25519");
uint8_t combined[crypto_sign_SECRETKEYBYTES];
memcpy(combined, work->key, sizeof(work->key));
memcpy(combined + sizeof(work->key), public_key, sizeof(public_key));
char combined_b64[512];
tf_base64_encode(combined, sizeof(combined), combined_b64, sizeof(combined_b64));
snprintf(combined_b64 + strlen(combined_b64), sizeof(combined_b64) - strlen(combined_b64), ".ed25519");
work->added = tf_ssb_db_identity_add(ssb, work->user, public_key_b64, combined_b64);
}
}
static void _tf_ssb_add_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
add_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_IsUndefined(work->result) ? (work->added ? JS_TRUE : JS_UNDEFINED) : work->result;
JSValue error = JS_Call(context, work->promise[work->added ? 0 : 1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static void _tf_ssb_add_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
add_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_add_identity_work, _tf_ssb_add_identity_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_add_identity_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_addIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
JSValue result = JS_UNDEFINED;
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
size_t user_length = user ? strlen(user) : 0;
JSValue buffer = JS_UNDEFINED;
size_t length = 0;
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[0]);
if (!array)
{
size_t offset;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[0], &offset, &length, &element_size);
if (!JS_IsException(buffer))
{
array = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
if (array)
{
if (length == crypto_sign_SECRETKEYBYTES / 2)
{
add_identity_t* work = tf_malloc(sizeof(add_identity_t) + user_length + 1);
*work = (add_identity_t) { .result = JS_UNDEFINED };
memcpy(work->key, array, sizeof(work->key));
if (user)
{
memcpy(work->user, user, user_length + 1);
}
result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_add", "Add an identity.", _tf_ssb_add_identity_permission_callback, work);
}
else
{
result = JS_ThrowInternalError(context, "Unexpected private key size: %d vs. %d\n", (int)length, crypto_sign_SECRETKEYBYTES);
}
}
else
{
result = JS_ThrowInternalError(context, "Expected array argument.");
}
JS_FreeValue(context, buffer);
JS_FreeCString(context, user);
return result;
}
typedef struct _delete_identity_t
{
char id[k_id_base64_len];
bool deleted;
JSValue result;
JSValue promise[2];
char user[];
} delete_identity_t;
static void _tf_ssb_delete_identity_work(tf_ssb_t* ssb, void* user_data)
{
delete_identity_t* work = user_data;
work->deleted = tf_ssb_db_identity_delete(ssb, work->user, work->id);
}
static void _tf_ssb_delete_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->deleted ? JS_TRUE : JS_FALSE;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static void _tf_ssb_delete_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
delete_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_delete_identity_work, _tf_ssb_delete_identity_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_delete_identity_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue process = data[0];
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
const char* id = JS_ToCString(context, argv[0]);
if (id && user)
{
size_t user_length = strlen(user);
delete_identity_t* work = tf_malloc(sizeof(delete_identity_t) + user_length + 1);
*work = (delete_identity_t) { .result = JS_UNDEFINED };
tf_string_set(work->id, sizeof(work->id), *id == '@' ? id + 1 : id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_delete", "Delete an identity.", _tf_ssb_delete_identity_permission_callback, work);
}
JS_FreeCString(context, id);
JS_FreeCString(context, user);
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
JSValue result;
JSValue promise[2];
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char user[];
} get_private_key_t;
static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
get_private_key_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
}
static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_private_key_t* work = user_data;
JSValue result = JS_UNDEFINED;
JSContext* context = work->context;
if (work->got_private_key && JS_IsUndefined(work->result))
{
result = tf_util_new_uint8_array(context, work->private_key, sizeof(work->private_key) / 2);
}
JSValue error = JS_Call(context, work->promise[work->got_private_key ? 0 : 1], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->result);
tf_free(work);
}
static void _tf_ssb_get_private_key_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
get_private_key_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_get_private_key_work, _tf_ssb_get_private_key_after_work, work);
}
else
{
work->result = JS_DupValue(context, value);
_tf_ssb_get_private_key_after_work(ssb, -1, work);
}
}
static JSValue _tf_ssb_getPrivateKey(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
size_t user_length = user ? strlen(user) : 0;
const char* id = JS_ToCString(context, argv[0]);
get_private_key_t* work = tf_malloc(sizeof(get_private_key_t) + user_length + 1);
*work = (get_private_key_t) { .context = context, .result = JS_UNDEFINED };
if (user)
{
memcpy(work->user, user, user_length + 1);
}
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_id_export", "Export a private key.", _tf_ssb_get_private_key_permission_callback, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -1758,6 +2031,9 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, ssb, "privateMessageEncrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_encrypt, 3, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "privateMessageDecrypt", JS_NewCFunctionData(context, _tf_ssb_private_message_decrypt, 2, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "appendMessageWithIdentity", JS_NewCFunctionData(context, _tf_ssb_appendMessageWithIdentity, 2, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "addIdentity", JS_NewCFunctionData(context, _tf_ssb_addIdentity, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "deleteIdentity", JS_NewCFunctionData(context, _tf_ssb_deleteIdentity, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getPrivateKey", JS_NewCFunctionData(context, _tf_ssb_getPrivateKey, 1, 0, 1, &process));
JS_FreeValue(context, ssb);
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");

View File

@@ -671,7 +671,7 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
JSValue user_permissions = name_string && !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++)
{
@@ -743,7 +743,6 @@ static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_d
tf_free((void*)cookie);
JS_FreeCString(context, name_string);
// tf_http_request_unref(request);
request->on_message = _httpd_app_on_message;
request->on_close = _httpd_app_on_close;
request->context = context;
@@ -769,18 +768,19 @@ void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
tf_free((void*)session);
JSValue credentials = JS_NewObject(context);
if (!JS_IsUndefined(jwt))
{
JSValue credentials = JS_NewObject(context);
JS_SetPropertyStr(context, credentials, "session", jwt);
tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}

View File

@@ -173,6 +173,7 @@ typedef struct _tf_ssb_timer_t
uv_timer_t timer;
void (*callback)(tf_ssb_t* ssb, void* user_data);
void* user_data;
char name[];
} tf_ssb_timer_t;
typedef struct _tf_ssb_broadcast_result_t
@@ -4583,7 +4584,9 @@ void tf_ssb_set_quiet(tf_ssb_t* ssb, bool quiet)
static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
{
tf_ssb_timer_t* timer = handle->data;
tf_trace_begin(timer->ssb->trace, timer->name);
timer->callback(timer->ssb, timer->user_data);
tf_trace_end(timer->ssb->trace);
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
}
@@ -4594,8 +4597,10 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
return;
}
const char* name = tf_util_function_to_string(callback);
size_t name_length = name ? strlen(name) : 0;
ssb->timers = tf_resize_vec(ssb->timers, sizeof(uv_timer_t*) * (ssb->timers_count + 1));
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t));
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t) + name_length + 1);
*timer = (tf_ssb_timer_t)
{
.ssb = ssb,
@@ -4606,6 +4611,8 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
.callback = callback,
.user_data = user_data,
};
tf_string_set(timer->name, name_length + 1, name);
tf_free((void*)name);
ssb->timers[ssb->timers_count++] = timer;
uv_timer_init(ssb->loop, &timer->timer);
uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0);

View File

@@ -171,6 +171,10 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
}
}
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_stats' AND NOT sql LIKE '%excluded.size%'"))
{
_tf_ssb_db_exec(db, "DROP TABLE messages_stats");
}
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
{
_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
@@ -186,10 +190,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"messages GROUP BY author");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
}
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_stats");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_stats AFTER INSERT ON messages BEGIN INSERT INTO messages_stats(author, max_sequence, max_timestamp, size) VALUES (new.author, "
"new.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, new.sequence), max_timestamp = MAX(max_timestamp, "
"new.timestamp), size = size + length(json(new.content)); END");
"new.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, excluded.max_sequence), max_timestamp = "
"MAX(max_timestamp, "
"excluded.max_timestamp), size = size + excluded.size; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_stats");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ad_stats AFTER DELETE ON messages BEGIN "

View File

@@ -96,209 +96,6 @@ static JSValue _tf_ssb_createIdentity(JSContext* context, JSValueConst this_val,
return result;
}
typedef struct _add_identity_t
{
uint8_t key[crypto_sign_SECRETKEYBYTES / 2];
bool added;
JSValue promise[2];
char user[];
} add_identity_t;
static void _tf_ssb_add_identity_work(tf_ssb_t* ssb, void* user_data)
{
add_identity_t* work = user_data;
uint8_t public_key[crypto_sign_PUBLICKEYBYTES];
unsigned char seed[crypto_sign_SEEDBYTES];
uint8_t secret_key[crypto_sign_SECRETKEYBYTES] = { 0 };
memcpy(secret_key, work->key, sizeof(secret_key) / 2);
if (crypto_sign_ed25519_sk_to_seed(seed, secret_key) == 0 && crypto_sign_seed_keypair(public_key, secret_key, seed) == 0)
{
char public_key_b64[512];
tf_base64_encode(public_key, sizeof(public_key), public_key_b64, sizeof(public_key_b64));
snprintf(public_key_b64 + strlen(public_key_b64), sizeof(public_key_b64) - strlen(public_key_b64), ".ed25519");
uint8_t combined[crypto_sign_SECRETKEYBYTES];
memcpy(combined, work->key, sizeof(work->key));
memcpy(combined + sizeof(work->key), public_key, sizeof(public_key));
char combined_b64[512];
tf_base64_encode(combined, sizeof(combined), combined_b64, sizeof(combined_b64));
snprintf(combined_b64 + strlen(combined_b64), sizeof(combined_b64) - strlen(combined_b64), ".ed25519");
work->added = tf_ssb_db_identity_add(ssb, work->user, public_key_b64, combined_b64);
}
}
static void _tf_ssb_add_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
add_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->added ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_addIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
JSValue buffer = JS_UNDEFINED;
size_t length = 0;
uint8_t* array = tf_util_try_get_array_buffer(context, &length, argv[1]);
if (!array)
{
size_t offset;
size_t element_size;
buffer = tf_util_try_get_typed_array_buffer(context, argv[1], &offset, &length, &element_size);
if (!JS_IsException(buffer))
{
array = tf_util_try_get_array_buffer(context, &length, buffer);
}
}
if (array)
{
if (length == crypto_sign_SECRETKEYBYTES / 2)
{
add_identity_t* work = tf_malloc(sizeof(add_identity_t) + user_length + 1);
*work = (add_identity_t) { 0 };
memcpy(work->key, array, sizeof(work->key));
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_add_identity_work, _tf_ssb_add_identity_after_work, work);
}
else
{
result = JS_ThrowInternalError(context, "Unexpected private key size: %d vs. %d\n", (int)length, crypto_sign_SECRETKEYBYTES);
}
}
else
{
result = JS_ThrowInternalError(context, "Expected array argument.");
}
JS_FreeValue(context, buffer);
JS_FreeCString(context, user);
}
return result;
}
typedef struct _delete_identity_t
{
char id[k_id_base64_len];
bool deleted;
JSValue promise[2];
char user[];
} delete_identity_t;
static void _tf_ssb_delete_identity_work(tf_ssb_t* ssb, void* user_data)
{
delete_identity_t* work = user_data;
work->deleted = tf_ssb_db_identity_delete(ssb, work->user, work->id);
}
static void _tf_ssb_delete_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->deleted ? JS_TRUE : JS_FALSE;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
if (id && user)
{
delete_identity_t* work = tf_malloc(sizeof(delete_identity_t) + user_length + 1);
*work = (delete_identity_t) { 0 };
tf_string_set(work->id, sizeof(work->id), *id == '@' ? id + 1 : id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_delete_identity_work, _tf_ssb_delete_identity_after_work, work);
}
JS_FreeCString(context, id);
JS_FreeCString(context, user);
}
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
JSValue promise[2];
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char user[];
} get_private_key_t;
static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
get_private_key_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
}
static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_private_key_t* work = user_data;
JSValue result = JS_UNDEFINED;
JSContext* context = work->context;
if (work->got_private_key)
{
result = tf_util_new_uint8_array(context, work->private_key, sizeof(work->private_key) / 2);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_getPrivateKey(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
get_private_key_t* work = tf_malloc(sizeof(get_private_key_t) + user_length + 1);
*work = (get_private_key_t) { .context = context };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_private_key_work, _tf_ssb_get_private_key_after_work, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
return result;
}
static JSValue _tf_ssb_getServerIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
@@ -1703,9 +1500,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
/* Requires an identity. */
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
JS_SetPropertyStr(context, object, "setUserPermission", JS_NewCFunction(context, _tf_ssb_set_user_permission, "setUserPermission", 5));
/* Does not require an identity. */

View File

@@ -114,6 +114,21 @@ static int _ssb_test_count_messages(tf_ssb_t* ssb)
return count.count;
}
static void _dump_stats_callback(JSValue row, void* user_data)
{
JSContext* context = tf_ssb_get_context(user_data);
JSValue json = JS_JSONStringify(context, row, JS_UNDEFINED, JS_UNDEFINED);
const char* string = JS_ToCString(context, json);
tf_printf("%s\n", string);
JS_FreeCString(context, string);
JS_FreeValue(context, json);
}
static void _ssb_test_dump_messages_stats(tf_ssb_t* ssb)
{
tf_ssb_db_visit_query(ssb, "SELECT * FROM messages_stats", JS_UNDEFINED, _dump_stats_callback, ssb);
}
static void _message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, void* user_data)
{
++*(int*)user_data;
@@ -248,6 +263,8 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uv_sleep(1000);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
@@ -258,6 +275,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
@@ -273,6 +291,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uint8_t* b0;
size_t s0 = 0;