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 \ docker.io \
doxygen \ doxygen \
file \ file \
firefox-geckodriver \
gcc-aarch64-linux-gnu \ gcc-aarch64-linux-gnu \
git \ git \
graphviz \ graphviz \
libgpgme11 \ libgpgme11 \
libssl-dev \ libssl-dev \
python3-selenium \
mingw-w64 \ mingw-w64 \
rsync \ rsync \
unzip \ 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; ./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 .. @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 .PHONY: appimage
flatpak: out/ ## Build a flatpak. flatpak: out/ ## Build a flatpak.
@@ -1300,6 +1319,8 @@ dist:
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk @cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage" @echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/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)" @echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/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)" @test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"

View File

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

View File

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

View File

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

View File

@@ -95,6 +95,22 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.root[0]; 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,9 +274,10 @@ class TfProfileElement extends LitElement {
if (this.id === this.whoami) { if (this.id === this.whoami) {
if (this.editing) { if (this.editing) {
edit = html` edit = html`
<div style="margin-top: 8px">
<button <button
id="save_profile" id="save_profile"
class="w3-button w3-theme-d1" class="w3-button w3-theme-l1"
@click=${this.save_edits} @click=${this.save_edits}
> >
Save Profile Save Profile
@@ -284,6 +285,7 @@ class TfProfileElement extends LitElement {
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard Discard
</button> </button>
</div>
`; `;
} else { } else {
edit = html`<button edit = html`<button
@@ -341,7 +343,7 @@ class TfProfileElement extends LitElement {
let description = this.editing?.description ?? profile.description; let description = this.editing?.description ?? profile.description;
return html` return html`
<style>${generate_theme()}</style> <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"> <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> <p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
</header> </header>
@@ -350,12 +352,14 @@ 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> <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> <button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div> </div>
<div style="display: flex; flex-direction: row; gap: 1em"> <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} ${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal"> <div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${ ${
image image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>` ? html`<div><img src=${'/' + image + '/view'} style="width: min(256px, 100%); height: auto"></img></div>`
: html`<div> : html`<div>
<div class="w3-jumbo">😎</div> <div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div> <div><i>Profile image not set.</i></div>
@@ -364,6 +368,8 @@ class TfProfileElement extends LitElement {
<div>${unsafeHTML(tfutils.markdown(description))}</div> <div>${unsafeHTML(tfutils.markdown(description))}</div>
</div> </div>
</div> </div>
${this.editing ? html`<footer class="w3-container w3-theme-l2"><p>${edit}</p></footer>` : undefined}
</div>
<div> <div>
Following ${profile.following} identities. Following ${profile.following} identities.
Followed by ${profile.followed} identities. Followed by ${profile.followed} 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"> <button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat">
Open Private Chat Open Private Chat
</button> </button>
${edit} ${this.editing ? undefined : edit}
${follow} ${follow}
${block} ${block}
</p> </p>

View File

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

View File

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

View File

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

View File

@@ -231,25 +231,9 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
user: getUser(process, process), user: getUser(process, process),
permissionTest: async function (permission, description) { permissionTest: async function (permission, description) {
let user = process?.credentials?.session?.name; let user = process?.credentials?.session?.name;
let settings = await loadSettings(); let permissions = await imports.core.permissionsGranted();
if (!user || !options?.packageOwner || !options?.packageName) { if (permissions && permissions[permission] !== undefined) {
return; if (permissions[permission]) {
} 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]
) {
return true; return true;
} else { } else {
throw Error(`Permission denied: ${permission}.`); 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)]) Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
); );
imports.ssb.createIdentity = () => process.createIdentity(); 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.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 ( if (
process.credentials && process.credentials &&
process.credentials.session && process.credentials.session &&
@@ -572,28 +517,6 @@ ssb_internal.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []); 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. * Send periodic stats to all clients.
*/ */

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -186,9 +186,9 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.39.6", "version": "6.39.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.6.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.7.tgz",
"integrity": "sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==", "integrity": "sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@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. * Crash fixes.
* Fix channels with hyphens and various other characters not working correctly. * 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. * Faster loads, though the first launch may be particularly slow as indexes are rebuilt.
* Fixed various broken links. * Fixed various broken links.
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1. * 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.db.h"
#include "ssb.h" #include "ssb.h"
#include "task.h" #include "task.h"
#include "trace.h"
#include "util.js.h" #include "util.js.h"
#include "quickjs.h" #include "quickjs.h"
@@ -890,13 +891,18 @@ typedef struct _permission_test_t
bool completed; bool completed;
permission_test_callback_t* callback; permission_test_callback_t* callback;
void* user_data; void* user_data;
char name[];
} permission_test_t; } permission_test_t;
static JSValue _tf_ssb_permission_test_resolve(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data) 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); permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true; 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); work->callback(context, true, argv[0], work->user_data);
tf_trace_end(trace);
return JS_UNDEFINED; 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); permission_test_t* work = JS_GetOpaque(data[0], s_permission_test_class_id);
work->completed = true; 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); work->callback(context, false, argv[0], work->user_data);
tf_trace_end(trace);
return JS_UNDEFINED; return JS_UNDEFINED;
} }
@@ -914,7 +924,11 @@ static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
if (!work->completed) if (!work->completed)
{ {
JSValue arg = JS_ThrowInternalError(work->context, "Permission test incomplete."); 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); work->callback(work->context, false, arg, work->user_data);
tf_trace_end(trace);
JS_FreeValue(work->context, arg); JS_FreeValue(work->context, arg);
} }
tf_free(work); 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); 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) { *payload = (permission_test_t) {
.context = context, .context = context,
.callback = callback, .callback = callback,
.user_data = user_data, .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); JSValue opaque = JS_NewObjectClass(context, s_permission_test_class_id);
JS_SetOpaque(opaque, payload); JS_SetOpaque(opaque, payload);
JSValue imports = JS_GetPropertyStr(context, process, "imports"); 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) static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue imports = argv[0]; 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, "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, "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, "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); JS_FreeValue(context, ssb);
JSValue credentials = JS_GetPropertyStr(context, process, "credentials"); 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); JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions); JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED; 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; int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++) 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); tf_free((void*)cookie);
JS_FreeCString(context, name_string); JS_FreeCString(context, name_string);
// tf_http_request_unref(request);
request->on_message = _httpd_app_on_message; request->on_message = _httpd_app_on_message;
request->on_close = _httpd_app_on_close; request->on_close = _httpd_app_on_close;
request->context = context; request->context = context;
@@ -769,10 +768,12 @@ void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session); JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
tf_free((void*)session); tf_free((void*)session);
JSValue credentials = JS_NewObject(context);
if (!JS_IsUndefined(jwt)) if (!JS_IsUndefined(jwt))
{ {
JSValue credentials = JS_NewObject(context);
JS_SetPropertyStr(context, credentials, "session", jwt); JS_SetPropertyStr(context, credentials, "session", jwt);
}
tf_http_request_ref(request); tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t)); app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) { *work = (app_t) {
@@ -782,5 +783,4 @@ void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
}; };
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer); 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_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; uv_timer_t timer;
void (*callback)(tf_ssb_t* ssb, void* user_data); void (*callback)(tf_ssb_t* ssb, void* user_data);
void* user_data; void* user_data;
char name[];
} tf_ssb_timer_t; } tf_ssb_timer_t;
typedef struct _tf_ssb_broadcast_result_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) static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
{ {
tf_ssb_timer_t* timer = handle->data; tf_ssb_timer_t* timer = handle->data;
tf_trace_begin(timer->ssb->trace, timer->name);
timer->callback(timer->ssb, timer->user_data); timer->callback(timer->ssb, timer->user_data);
tf_trace_end(timer->ssb->trace);
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close); 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; 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)); 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) *timer = (tf_ssb_timer_t)
{ {
.ssb = ssb, .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, .callback = callback,
.user_data = user_data, .user_data = user_data,
}; };
tf_string_set(timer->name, name_length + 1, name);
tf_free((void*)name);
ssb->timers[ssb->timers_count++] = timer; ssb->timers[ssb->timers_count++] = timer;
uv_timer_init(ssb->loop, &timer->timer); uv_timer_init(ssb->loop, &timer->timer);
uv_timer_start(&timer->timer, _tf_ssb_scheduled_timer, delay_ms, 0); 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"); _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')")) if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
{ {
_tf_ssb_db_exec(db, "BEGIN TRANSACTION"); _tf_ssb_db_exec(db, "BEGIN TRANSACTION");
@@ -186,10 +190,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"messages GROUP BY author"); "messages GROUP BY author");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION"); _tf_ssb_db_exec(db, "COMMIT TRANSACTION");
} }
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_stats");
_tf_ssb_db_exec(db, _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, " "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.sequence, new.timestamp, length(json(new.content))) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, excluded.max_sequence), max_timestamp = "
"new.timestamp), size = size + length(json(new.content)); END"); "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, "DROP TRIGGER IF EXISTS messages_ad_stats");
_tf_ssb_db_exec(db, _tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ad_stats AFTER DELETE ON messages BEGIN " "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; 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) static JSValue _tf_ssb_getServerIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
@@ -1703,9 +1500,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
/* Requires an identity. */ /* Requires an identity. */
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1)); 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)); JS_SetPropertyStr(context, object, "setUserPermission", JS_NewCFunction(context, _tf_ssb_set_user_permission, "setUserPermission", 5));
/* Does not require an identity. */ /* Does not require an identity. */

View File

@@ -114,6 +114,21 @@ static int _ssb_test_count_messages(tf_ssb_t* ssb)
return count.count; 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) static void _message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, void* user_data)
{ {
++*(int*)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); JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored); _wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj); JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uv_sleep(1000);
obj = JS_NewObject(context0); obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post")); 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); JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored); _wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj); JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
obj = JS_NewObject(context0); obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post")); 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); JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored); _wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj); JS_FreeValue(context0, obj);
_ssb_test_dump_messages_stats(ssb0);
uint8_t* b0; uint8_t* b0;
size_t s0 = 0; size_t s0 = 0;