31 Commits

Author SHA1 Message Date
90b0c22f87 docs: Clean up changelog.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-29 12:03:11 -05:00
07e5188525 bookclub: Fix the bookclub app showing nothing for no bookclub messages.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-29 11:57:50 -05:00
bb1fcd8bb9 build: Let's build 0.2025.12.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m41s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-29 11:53:14 -05:00
83d9e5fd48 ssb: Fix an intermittent leak observed while running tests. 2025-12-29 11:52:59 -05:00
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
938f728eb9 build: Just curious, can the CI worker run headless selenium tests?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 12s
2025-12-25 11:03:22 -05:00
6e8a0031a8 bookclub: Handle both about and bookclubUpdate messages. 2025-12-25 10:38:18 -05:00
085f62aadf core: Move ssb.appendMessageWithIdentity() from JS => C, and fix a permission test lifetime issue along the way.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-23 17:12:38 -05:00
71d556143b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m24s
2025-12-23 16:12:23 -05:00
081bff9a26 ssb: Load names and profile info for users we see who we're not following.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-23 16:09:47 -05:00
f2f4455c82 ssb: Replication fun. Don't request blobs until messages are up to date. Fix multiple issues with blob wants determinating. Fix issues resulting from message added callbacks being merged.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m54s
2025-12-23 12:54:01 -05:00
29 changed files with 905 additions and 668 deletions

View File

@@ -17,7 +17,7 @@ MAKEFLAGS += --no-builtin-rules
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 49
VERSION_NUMBER := 0.2025.12-wip
VERSION_NUMBER := 0.2025.12
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.5
@@ -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": "&EO5ifwzemEeSJsN6SJ2VTyE+sqnwU2gikIngQimwnDo=.sha256"
"emoji": "📖",
"previous": "&Vt7LWYGm9HpVM50+aBJv9Q1FnEf12Gd1+Uyft+IYWGo=.sha256"
}

View File

@@ -46,12 +46,25 @@ async function main() {
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.about', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?) AS book, json_each(?) AS following
FROM messages, json_each(messages.content) AS fields, json_each(?1) AS book, json_each(?2) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'about'
AND messages.content ->> '$.about' = book.value
AND NOT fields.key IN ('about', 'type')
UNION
SELECT
messages.author,
messages.content ->> '$.updates' AS about,
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.updates', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?1) AS book, json_each(?2) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'bookclubUpdate'
AND messages.content ->> '$.updates' = book.value
AND NOT fields.key IN ('about', 'updates', 'type')
) WHERE rank = 1
GROUP BY author, about
`,

View File

@@ -3,8 +3,8 @@ import {markdown} from './markdown.js';
class BookClubElement extends LitElement {
render() {
if (!g_data) {
return html`<h1>No bookclub messages found.</h1>`;
if (!g_data?.length) {
return html`<h1>No bookclub messages to display.</h1>`;
}
return html`
<link rel="stylesheet" href="w3.css"></link>

View File

@@ -6,7 +6,7 @@
const g_data = G_DATA;
</script>
</head>
<body>
<body style="background-color: #fff">
<bc-app />
</body>
<script type="module" src="bc-app.js"></script>

View File

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

View File

@@ -236,7 +236,7 @@ class TfElement extends LitElement {
}
}
async fetch_about(following, users) {
async fetch_about(following, users, transient) {
this.loading_about++;
let ids = Object.keys(following).sort();
const k_cache_version = 3;
@@ -320,7 +320,7 @@ class TfElement extends LitElement {
this.loading_about--;
let new_cache = JSON.stringify(cache);
if (new_cache != original_cache) {
if (!transient && new_cache != original_cache) {
let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000);
@@ -802,6 +802,18 @@ class TfElement extends LitElement {
}
}
async request_user(event) {
let users = {};
users[event.detail.id] = {};
users = await this.fetch_user_info(users);
if (this.users[event.detail.id]?.seq !== users[event.detail.id]?.seq) {
let self = this;
this.fetch_about(users, users, true).then(function (result) {
self.users = Object.assign({}, self.users, users);
});
}
}
render() {
let self = this;
@@ -854,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 ? '🔗' : '⛓️‍💥'}
@@ -914,6 +930,7 @@ class TfElement extends LitElement {
<div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"
@tf-request-user=${this.request_user}
>
${progress}
<div style="flex: 0 0">${tabs}</div>

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

@@ -25,6 +25,15 @@ class TfUserElement extends LitElement {
render() {
let user = this.users[this.id];
if (!this.users[this.id]) {
this.dispatchEvent(
new CustomEvent('tf-request-user', {
bubbles: true,
composed: true,
detail: {id: this.id},
})
);
}
let shape =
user?.follow_depth === undefined || user.follow_depth >= 2
? 'w3-circle'

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,73 +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);
});
}
};
imports.ssb.appendMessageWithIdentity = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
let action;
try {
if (message?.type === 'vote' && message?.vote?.expression) {
action = `React with ${message?.vote?.expression}.`;
} else if (typeof message === 'string') {
action = `Post a private message.`;
} else {
action = `Publish ${'aeiou'.indexOf(message?.type?.toLowerCase()?.substring(0, 1)) != -1 ? 'an' : 'a'} "${message?.type}" message.`;
}
} catch {}
return Promise.resolve(
imports.core.permissionTest('ssb_append', action)
).then(function () {
return ssb.appendMessageWithIdentity(
process.credentials.session.name,
id,
message
);
});
}
};
if (
process.credentials &&
process.credentials.session &&
@@ -599,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

222
deps/codemirror_src/package-lock.json generated vendored
View File

@@ -129,14 +129,14 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
@@ -165,9 +165,9 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -186,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.39.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz",
"integrity": "sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==",
"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",
@@ -248,9 +248,9 @@
}
},
"node_modules/@lezer/common": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz",
"integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
@@ -274,9 +274,9 @@
}
},
"node_modules/@lezer/html": {
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
"integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -316,12 +316,12 @@
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.1.tgz",
"integrity": "sha512-72ah+Sml7lD8Wn7lnz9vwYmZBo9aQT+I2gjK/0epI+gjdwUbWw3MJ/ZBGEqG1UfrIauRqH37/c5mVHXeCTGXtA==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.2.tgz",
"integrity": "sha512-iNSdKrIK0FfOjVPVpV0fu7OykdncYpEzf4vkG9szFf60ql/ObZShoVbM9u1tgkogDOmubms1CyoNS2/unOXWNw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0"
}
},
@@ -412,9 +412,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"cpu": [
"arm"
],
@@ -425,9 +425,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"cpu": [
"arm64"
],
@@ -438,9 +438,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"cpu": [
"arm64"
],
@@ -451,9 +451,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"cpu": [
"x64"
],
@@ -464,9 +464,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"cpu": [
"arm64"
],
@@ -477,9 +477,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"cpu": [
"x64"
],
@@ -490,9 +490,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"cpu": [
"arm"
],
@@ -503,9 +503,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"cpu": [
"arm"
],
@@ -516,9 +516,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"cpu": [
"arm64"
],
@@ -529,9 +529,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz",
"integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"cpu": [
"arm64"
],
@@ -542,9 +542,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"cpu": [
"loong64"
],
@@ -555,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"cpu": [
"ppc64"
],
@@ -568,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"cpu": [
"riscv64"
],
@@ -581,9 +581,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"cpu": [
"riscv64"
],
@@ -594,9 +594,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"cpu": [
"s390x"
],
@@ -607,9 +607,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
@@ -620,9 +620,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
@@ -633,9 +633,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"cpu": [
"arm64"
],
@@ -646,9 +646,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"cpu": [
"arm64"
],
@@ -659,9 +659,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"cpu": [
"ia32"
],
@@ -672,9 +672,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"cpu": [
"x64"
],
@@ -685,9 +685,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"cpu": [
"x64"
],
@@ -877,9 +877,9 @@
}
},
"node_modules/rollup": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz",
"integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -892,28 +892,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.5",
"@rollup/rollup-android-arm64": "4.53.5",
"@rollup/rollup-darwin-arm64": "4.53.5",
"@rollup/rollup-darwin-x64": "4.53.5",
"@rollup/rollup-freebsd-arm64": "4.53.5",
"@rollup/rollup-freebsd-x64": "4.53.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
"@rollup/rollup-linux-arm64-musl": "4.53.5",
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
"@rollup/rollup-linux-x64-gnu": "4.53.5",
"@rollup/rollup-linux-x64-musl": "4.53.5",
"@rollup/rollup-openharmony-arm64": "4.53.5",
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
"@rollup/rollup-win32-x64-gnu": "4.53.5",
"@rollup/rollup-win32-x64-msvc": "4.53.5",
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"fsevents": "~2.3.2"
}
},

View File

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

View File

@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends"
android:versionCode="49"
android:versionName="0.2025.12-wip">
android:versionName="0.2025.12">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application

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"
@@ -24,6 +25,8 @@
#include <alloca.h>
#endif
static JSClassID s_permission_test_class_id;
typedef struct _app_path_pair_t
{
const char* app;
@@ -884,38 +887,76 @@ typedef void(permission_test_callback_t)(JSContext* context, bool granted, JSVal
typedef struct _permission_test_t
{
JSContext* context;
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)
{
JSClassID class_id = 0;
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
JS_FreeValue(context, data[0]);
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_free(work);
tf_trace_end(trace);
return JS_UNDEFINED;
}
static JSValue _tf_ssb_permission_test_reject(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSClassID class_id = 0;
permission_test_t* work = JS_GetAnyOpaque(data[0], &class_id);
JS_FreeValue(context, data[0]);
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_free(work);
tf_trace_end(trace);
return JS_UNDEFINED;
}
static void _tf_ssb_permission_test_finalizer(JSRuntime* runtime, JSValue value)
{
permission_test_t* work = JS_GetOpaque(value, s_permission_test_class_id);
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);
}
static void _tf_ssb_permission_test(JSContext* context, JSValue process, const char* permission, const char* description, permission_test_callback_t* callback, void* user_data)
{
permission_test_t* payload = tf_malloc(sizeof(permission_test_t));
if (!s_permission_test_class_id)
{
JSClassDef def = {
.class_name = "permission_test",
.finalizer = _tf_ssb_permission_test_finalizer,
};
JS_NewClassID(&s_permission_test_class_id);
JS_NewClass(JS_GetRuntime(context), s_permission_test_class_id, &def);
}
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,
};
JSValue opaque = JS_NewObject(context);
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");
JSValue core = JS_GetPropertyStr(context, imports, "core");
@@ -934,10 +975,11 @@ static void _tf_ssb_permission_test(JSContext* context, JSValue process, const c
JS_FreeValue(context, result);
result = JS_Call(context, catch, promise, 1, &reject);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, opaque);
JS_FreeValue(context, promise);
JS_FreeValue(context, resolve);
JS_FreeValue(context, reject);
JS_FreeValue(context, result);
JS_FreeValue(context, then);
JS_FreeValue(context, catch);
for (int i = 0; i < tf_countof(args); i++)
@@ -1577,6 +1619,388 @@ static JSValue _tf_ssb_private_message_decrypt(JSContext* context, JSValueConst
return result;
}
typedef struct _append_message_t
{
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char previous_id[512];
int32_t previous_sequence;
JSContext* context;
JSValue promise[2];
JSValue message;
char user[];
} append_message_t;
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
{
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(async->context, error);
JS_FreeValue(async->context, error);
JS_FreeValue(async->context, async->message);
JS_FreeValue(async->context, async->promise[0]);
JS_FreeValue(async->context, async->promise[1]);
tf_free(async);
}
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
{
append_message_t* async = user_data;
JSValue result = JS_UNDEFINED;
if (verified)
{
result = is_new ? JS_TRUE : JS_FALSE;
}
_tf_ssb_appendMessage_finish(async, verified, result);
}
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
{
append_message_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));
}
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
append_message_t* work = user_data;
if (work->got_private_key)
{
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
JS_FreeValue(work->context, signed_message);
}
else
{
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
}
}
static void _tf_ssb_append_message_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
append_message_t* work = user_data;
if (granted)
{
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
}
else
{
_tf_ssb_appendMessage_finish(work, false, value);
}
}
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue process = data[0];
char description[1024] = "Publish a new message.";
const char* type = tf_util_get_property_as_string(context, argv[1], "type");
if (type)
{
if (strcmp(type, "vote") == 0)
{
JSValue vote = JS_GetPropertyStr(context, argv[1], "vote");
const char* expression = tf_util_get_property_as_string(context, vote, "expression");
snprintf(description, sizeof(description), "React with %s.", expression);
JS_FreeCString(context, expression);
JS_FreeValue(context, vote);
}
else
{
snprintf(description, sizeof(description), "Publish a new %s message.", type);
}
}
else if (JS_IsString(argv[1]))
{
tf_string_set(description, sizeof(description), "Publish a new private message.");
}
JS_FreeCString(context, type);
const char* user = _tf_ssb_get_process_credentials_session_name(context, process);
if (!user)
{
return JS_ThrowInternalError(context, "Invalid user.");
}
else
{
size_t user_length = strlen(user);
const char* id = JS_ToCString(context, argv[0]);
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[1]) };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
_tf_ssb_permission_test(context, process, "ssb_append", description, _tf_ssb_append_message_permission_callback, work);
return result;
}
}
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];
@@ -1606,6 +2030,10 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, ssb, "getOwnerIdentities", JS_NewCFunctionData(context, _tf_ssb_getOwnerIdentities, 0, 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, "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
@@ -2688,12 +2689,6 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
uv_close((uv_handle_t*)&ssb->timers[i]->timer, _tf_ssb_on_timer_close);
}
if (ssb->connections_tracker)
{
tf_ssb_connections_destroy(ssb->connections_tracker);
ssb->connections_tracker = NULL;
}
if (!ssb->quiet)
{
tf_printf("Waiting for closes.\n");
@@ -2718,6 +2713,12 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_printf("Waiting for rpc.\n");
}
if (ssb->ref_count == 0 && ssb->connections_tracker)
{
tf_ssb_connections_destroy(ssb->connections_tracker);
ssb->connections_tracker = NULL;
}
while (ssb->rpc)
{
tf_ssb_rpc_callback_node_t* node = ssb->rpc;
@@ -2883,7 +2884,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
}
ssb->shutting_down_deferred = true;
if (ssb->connection_ref_count == 0 && ssb->db_ref_count == 0)
if (ssb->connection_ref_count == 0 && ssb->db_ref_count == 0 && ssb->ref_count == 0)
{
uv_mutex_destroy(&ssb->db_readers_lock);
uv_mutex_destroy(&ssb->db_writer_lock);
@@ -4311,7 +4312,14 @@ void tf_ssb_ref(tf_ssb_t* ssb)
void tf_ssb_unref(tf_ssb_t* ssb)
{
int new_count = --ssb->ref_count;
if (new_count < 0)
if (new_count == 0)
{
if (ssb->shutting_down_deferred)
{
tf_ssb_destroy(ssb);
}
}
else if (new_count < 0)
{
tf_printf("tf_ssb_unref past 0: %d\n", new_count);
abort();
@@ -4583,7 +4591,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 +4604,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 +4618,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

@@ -2,6 +2,7 @@
#include "log.h"
#include "mem.h"
#include "ssb.ebt.h"
#include "ssb.h"
#include "trace.h"
#include "util.js.h"
@@ -170,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");
@@ -185,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 "
@@ -440,11 +447,12 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_id_idx ON blob_wants_cache (id)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS blob_wants_cache_timestamp_id_idx ON blob_wants_cache (timestamp, id)");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ai_blob_wants_cache");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_blob_wants_cache AFTER INSERT ON messages_refs BEGIN "
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blobs ON new.ref = blobs.id "
"LEFT OUTER JOIN blobs ON new.ref = blobs.id "
"WHERE messages.id = new.message AND "
"LENGTH(new.ref) = 52 AND new.ref LIKE '&%.sha256' AND "
"blobs.content IS NULL "
@@ -455,7 +463,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"INSERT INTO blob_wants_cache (source, id, timestamp) "
"SELECT messages.id, new.ref, messages.timestamp FROM messages "
"JOIN blob_wants_cache bwc ON bwc.source = messages.id AND bwc.id = new.blob "
"JOIN blobs ON bwc.id = blobs.id "
"LEFT OUTER JOIN blobs ON bwc.id = blobs.id "
"WHERE blobs.content IS NULL "
"ON CONFLICT (source, id) DO NOTHING; END");
_tf_ssb_db_exec(db,
@@ -534,46 +542,36 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
return exists;
}
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp, const char* content,
size_t content_len, const char* signature, int flags)
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, sqlite3_stmt* statement, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp,
const char* content, size_t content_len, const char* signature, int flags)
{
int64_t last_row_id = -1;
bool id_mismatch = false;
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
{
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING";
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{
int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
}
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
{
last_row_id = sqlite3_last_insert_rowid(db);
}
tf_printf("_tf_ssb_db_store_message_raw: %s\n", sqlite3_errmsg(db));
}
else
if (r == SQLITE_DONE && sqlite3_changes(db) != 0)
{
tf_printf("bind failed\n");
last_row_id = sqlite3_last_insert_rowid(db);
}
sqlite3_finalize(statement);
}
else
{
tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
tf_printf("bind failed: %s\n", sqlite3_errmsg(db));
}
sqlite3_reset(statement);
}
else if (id_mismatch)
{
@@ -665,16 +663,27 @@ static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
bool in_transaction = _tf_ssb_db_try_exec(db, "BEGIN TRANSACTION") == SQLITE_OK;
while (store)
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING";
sqlite3_stmt* insert_statement = NULL;
if (sqlite3_prepare_v2(db, query, -1, &insert_statement, NULL) == SQLITE_OK)
{
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp,
store->content, store->length, store->signature, store->flags);
if (last_row_id != -1)
while (store)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
int64_t last_row_id = _tf_ssb_db_store_message_raw(db, insert_statement, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence,
store->timestamp, store->content, store->length, store->signature, store->flags);
if (last_row_id != -1)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(db, last_row_id);
}
store = store->next;
}
store = store->next;
sqlite3_finalize(insert_statement);
}
else
{
tf_printf("prepare failed: %s.\n", sqlite3_errmsg(db));
}
if (in_transaction)
@@ -760,6 +769,22 @@ static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void*
store = store->next;
}
tf_ssb_connection_t* connections[256];
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
store = user_data;
while (store)
{
for (int i = 0; i < count; i++)
{
tf_ssb_connection_t* connection = connections[i];
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
tf_ssb_ebt_set_messages_received(tf_ssb_connection_get_ebt(connections[i]), store->author, store->sequence);
}
}
store = store->next;
}
if (last_stored)
{
tf_trace_begin(trace, "notify_message_added");

View File

@@ -18,8 +18,6 @@ static const int k_sql_async_timeout_ms = 60 * 1000;
static JSClassID _tf_ssb_classId;
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
typedef struct _create_identity_t
{
char id[k_id_base64_len];
@@ -98,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;
@@ -390,92 +185,6 @@ static JSValue _tf_ssb_getIdentityInfo(JSContext* context, JSValueConst this_val
return result;
}
typedef struct _append_message_t
{
char id[k_id_base64_len];
uint8_t private_key[crypto_sign_SECRETKEYBYTES];
bool got_private_key;
char previous_id[512];
int32_t previous_sequence;
JSContext* context;
JSValue promise[2];
JSValue message;
char user[];
} append_message_t;
static void _tf_ssb_appendMessage_finish(append_message_t* async, bool success, JSValue result)
{
JSValue error = JS_Call(async->context, success ? async->promise[0] : async->promise[1], JS_UNDEFINED, 1, &result);
tf_util_report_error(async->context, error);
JS_FreeValue(async->context, error);
JS_FreeValue(async->context, async->message);
JS_FreeValue(async->context, async->promise[0]);
JS_FreeValue(async->context, async->promise[1]);
tf_free(async);
}
static void _tf_ssb_appendMessageWithIdentity_callback(const char* id, bool verified, bool is_new, void* user_data)
{
append_message_t* async = user_data;
JSValue result = JS_UNDEFINED;
if (verified)
{
result = is_new ? JS_TRUE : JS_FALSE;
}
_tf_ssb_appendMessage_finish(async, verified, result);
}
static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, void* user_data)
{
append_message_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));
}
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
static void _tf_ssb_append_message_with_identity_get_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
append_message_t* work = user_data;
if (work->got_private_key)
{
JSValue signed_message = tf_ssb_sign_message(ssb, work->id, work->private_key, work->message, work->previous_id, work->previous_sequence);
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _tf_ssb_appendMessageWithIdentity_callback, work);
JS_FreeValue(work->context, signed_message);
}
else
{
_tf_ssb_appendMessage_finish(work, false, JS_ThrowInternalError(work->context, "Unable to get private key for user %s with identity %s.", work->user, work->id));
}
}
static JSValue _tf_ssb_appendMessageWithIdentity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
if (!ssb)
{
return JS_ThrowInternalError(context, "No SSB instance.");
}
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
append_message_t* work = tf_malloc(sizeof(append_message_t) + user_length + 1);
*work = (append_message_t) { .context = context, .message = JS_DupValue(context, argv[2]) };
memcpy(work->user, user, user_length + 1);
tf_string_set(work->id, sizeof(work->id), id);
JS_FreeCString(context, id);
JS_FreeCString(context, user);
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_append_message_with_identity_get_key_work, _tf_ssb_append_message_with_identity_get_key_after_work, work);
return result;
}
typedef struct _blob_get_t
{
JSContext* context;
@@ -1791,12 +1500,7 @@ 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));
/* Write. */
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
/* Does not require an identity. */
JS_SetPropertyStr(context, object, "getServerIdentity", JS_NewCFunction(context, _tf_ssb_getServerIdentity, "getServerIdentity", 0));

View File

@@ -190,15 +190,31 @@ static void _tf_ssb_rpc_blobs_has(tf_ssb_connection_t* connection, uint8_t flags
JS_FreeValue(context, ids);
}
static bool _tf_ssb_rpc_are_messages_pending_in(tf_ssb_connection_t* connection)
{
int in_pending = 0;
int in_total = 0;
int out_pending = 0;
int out_total = 0;
if (connection)
{
tf_ssb_ebt_get_progress(tf_ssb_connection_get_ebt(connection), &in_pending, &in_total, &out_pending, &out_total);
}
return in_pending != 0;
}
static void _tf_ssb_rpc_blob_wants_added_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{
tf_ssb_connection_t* connection = user_data;
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, id, JS_NewInt64(context, -1));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
if (!_tf_ssb_rpc_are_messages_pending_in(connection))
{
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, id, JS_NewInt64(context, -1));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
}
typedef struct _blob_wants_work_t
@@ -235,7 +251,8 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
if (sqlite3_bind_text(statement, 1, blob_wants->last_id, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, timestamp) == SQLITE_OK &&
sqlite3_bind_int(statement, 3, tf_countof(work->out_id)) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
int r = SQLITE_OK;
while ((r = sqlite3_step(statement)) == SQLITE_ROW)
{
tf_string_set(work->out_id[work->out_id_count], sizeof(work->out_id[work->out_id_count]), (const char*)sqlite3_column_text(statement, 0));
work->out_id_count++;
@@ -264,7 +281,10 @@ static void _tf_ssb_request_blob_wants_after_work(tf_ssb_connection_t* connectio
JS_SetPropertyStr(context, message, work->out_id[i], JS_NewInt32(context, -1));
send_failed = !tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
blob_wants->wants_sent++;
if (!send_failed)
{
blob_wants->wants_sent++;
}
}
if (work->out_id_count)
{
@@ -276,9 +296,12 @@ static void _tf_ssb_request_blob_wants_after_work(tf_ssb_connection_t* connectio
static void _tf_ssb_rpc_request_more_blobs(tf_ssb_connection_t* connection)
{
blob_wants_work_t* work = tf_malloc(sizeof(blob_wants_work_t));
memset(work, 0, sizeof(*work));
tf_ssb_connection_run_work(connection, _tf_ssb_request_blob_wants_work, _tf_ssb_request_blob_wants_after_work, work);
if (tf_ssb_connection_get_blob_wants_state(connection)->wants_sent == 0 && !_tf_ssb_rpc_are_messages_pending_in(connection))
{
blob_wants_work_t* work = tf_malloc(sizeof(blob_wants_work_t));
memset(work, 0, sizeof(*work));
tf_ssb_connection_run_work(connection, _tf_ssb_request_blob_wants_work, _tf_ssb_request_blob_wants_after_work, work);
}
}
static void _tf_ssb_rpc_blobs_createWants(
@@ -718,10 +741,12 @@ static void _tf_ssb_rpc_connection_blobs_createWants_callback(
}
int64_t size = 0;
JS_ToInt64(context, &size, key_value);
if (--blob_wants->wants_sent == 0)
/* We don't have the context here to disambiguate responses to incoming requests. */
if (blob_wants->wants_sent > 0)
{
_tf_ssb_rpc_request_more_blobs(connection);
--blob_wants->wants_sent;
}
_tf_ssb_rpc_request_more_blobs(connection);
if (size < 0)
{
blob_create_wants_work_t* work = tf_malloc(sizeof(blob_create_wants_work_t));
@@ -1940,8 +1965,8 @@ static void _tf_ssb_rpc_message_added_callback(tf_ssb_t* ssb, const char* author
tf_ssb_connection_t* connection = connections[i];
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
tf_ssb_ebt_set_messages_received(tf_ssb_connection_get_ebt(connections[i]), author, sequence);
_tf_ssb_rpc_ebt_schedule_send_clock(connections[i]);
_tf_ssb_rpc_request_more_blobs(connection);
}
}
}

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;

View File

@@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.2025.12-wip"
#define VERSION_NUMBER "0.2025.12"
#define VERSION_NAME "This program kills fascists."

View File

@@ -67,7 +67,7 @@ success = False
try:
options = webdriver.FirefoxOptions()
service = Service(log_output = 'out/geckodriver.log')
#options.add_argument('--headless')
options.add_argument('--headless')
driver = webdriver.Firefox(options = options, service = service)
wait = WebDriverWait(driver, 10)