16 Commits

Author SHA1 Message Date
d873d99b23 ssb: Handful of URL encoding issues.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m9s
2025-12-15 20:42:57 -05:00
1a5392d942 ssb: Avoid an unnecessary messages load.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m54s
2025-12-15 12:30:27 -05:00
ef80c0910c intro: Scroll to top when switching pages.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 15m36s
2025-12-13 09:01:17 -05:00
6c641acdd3 ssb: Put the hamburger menu on the same line as the welcome text. 2025-12-13 08:57:06 -05:00
f0babc6f95 core: Fix a recently introduced use after free.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 11m44s
2025-12-12 18:24:23 -05:00
1382eac7e5 ssb: Paranoia around trying to avoid showing stale/irrelevant messages. Need to rethink this approach entirely sometime.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m27s
Build Tilde Friends / Build-All (push) Successful in 9m55s
2025-12-11 22:05:06 -05:00
79b7252a27 ssb: Be much more generous about what's allowed in a hashtag ref. Fixes #dev-diary not behaving correctly as a channel.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-11 22:01:38 -05:00
2e8402d11d update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 11m8s
2025-12-11 12:48:44 -05:00
c34065795c build: Get iOS and Android on the same versionCode/CFBundleVersion.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m2s
2025-12-10 12:34:57 -05:00
1463c18c12 core: Unused.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m29s
2025-12-10 12:28:44 -05:00
f39b0977b7 build: Fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m1s
2025-12-09 21:33:03 -05:00
8f9824e9b7 core: Minor simplification around getting account name.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 3m24s
2025-12-09 20:30:09 -05:00
33392e7c55 core: Move ssb.swapWithServerIdentity() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 20:21:02 -05:00
b4c014fd27 core: Remove some ancient unused resizeMe, setHash, and storeBlob message handlers.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-09 19:05:07 -05:00
81353b4da9 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m8s
2025-12-09 18:48:08 -05:00
d67297c35b ssb: Better search feedback. 2025-12-09 18:43:52 -05:00
19 changed files with 270 additions and 289 deletions

View File

@@ -17,7 +17,6 @@ MAKEFLAGS += --no-builtin-rules
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 49
VERSION_CODE_IOS := 27
VERSION_NUMBER := 0.2025.12-wip
VERSION_NAME := This program kills fascists.
@@ -893,7 +892,7 @@ src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
tr '\n' '^' | \
sed -r \
-e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE_IOS)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE)\2@' \
-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
tr '^' '\n' > \
$@.tmp && mv $@.tmp $@ || rm -f $@.tmp

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💡",
"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
"previous": "&FGkkfFLaEID3V4lUjPbgCOwgEvNXkcVkzs0zzwD/gQ8=.sha256"
}

View File

@@ -34,6 +34,7 @@
class="w3-flex w3-dark-gray w3-center"
>
<div
id="scrollbox"
style="
flex: 1 1 auto;
overflow: auto;
@@ -251,6 +252,7 @@
index == 0 ? 'hidden' : 'visible';
document.getElementById('right').style.visibility =
index == slides.length - 1 ? 'hidden' : 'visible';
document.getElementById('scrollbox').scrollTo(0, 0);
}
let dots = [...document.getElementsByClassName('dot')];

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&Do4vIjdE5vJgJ+fIZ10zOeDQcqNd+VUacQl2wzRjGhw=.sha256"
"previous": "&KPnjURiuJa5b0ONjxz11bMm7yuhY9wlBTyB+fzl0zzk=.sha256"
}

View File

@@ -707,9 +707,7 @@ class TfElement extends LitElement {
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
query=${this.search_text()}
></tf-tab-search>
`;
}
@@ -758,7 +756,7 @@ class TfElement extends LitElement {
search_text.focus();
this.set_tab('search');
} else {
this.set_hash('#q=' + search_text.value);
this.set_hash('#q=' + encodeURIComponent(search_text.value));
}
}
@@ -768,6 +766,16 @@ class TfElement extends LitElement {
}
}
search_text() {
if (this.hash.startsWith('#q=')) {
try {
return decodeURIComponent(this.hash.substring('#q='.length));
} catch {
return this.hash.substring('#q='.length);
}
}
}
render() {
let self = this;
@@ -832,7 +840,7 @@ class TfElement extends LitElement {
: undefined
}
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>🔍<span class="w3-hide-small">Search</span></button>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown}></input>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown} value=${this.search_text()}></input>
</div>
`;
let contents = this.guest

View File

@@ -398,16 +398,23 @@ class TfTabNewsFeedElement extends LitElement {
);
}
make_messages_key() {
return JSON.stringify([
this.hash,
Object.keys(this.channels_latest ?? {}).filter((x) => x != '🔐'),
]);
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
let original_hash = this.hash;
let original_key = this.make_messages_key();
try {
if (this._messages_hash !== this.hash) {
if (this._messages_key !== original_key) {
this.messages = [];
this._messages_hash = this.hash;
this._messages_key = original_key;
}
this._messages_following = JSON.stringify(this.following);
this._private_messages = JSON.stringify([
@@ -429,7 +436,8 @@ class TfTabNewsFeedElement extends LitElement {
} finally {
this.loading--;
}
if (this.hash == original_hash) {
let current_key = this.make_messages_key();
if (current_key === original_key) {
this.messages = this.merge_messages(this.messages, messages);
}
this.time_loading = undefined;
@@ -485,18 +493,18 @@ class TfTabNewsFeedElement extends LitElement {
render() {
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_key !== this.make_messages_key() ||
this._messages_following !== JSON.stringify(this.following) ||
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]) ||
this._channels_latest !==
JSON.stringify(Object.keys(this.channels_latest))
(this.hash.startsWith('#🔐') &&
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]))
) {
console.log(this._messages_key, this.make_messages_key());
console.log(
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_hash != this.hash} following=${this._messages_following !== JSON.stringify(this.following)}, channels=${this._channels_latest !== JSON.stringify(Object.keys(this.channels_latest))}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
);
this.load_messages();
}

View File

@@ -428,18 +428,18 @@ class TfTabNewsElement extends LitElement {
</p>
<div>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</div>
<span
style="display: inline-block; width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
style="width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
>
<button
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</button>
Welcome,
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</span>
</div>
${edit_profile}
</div>
<div>

View File

@@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js';
@@ -44,36 +44,37 @@ class TfTabSearchElement extends LitElement {
this.error = undefined;
this.results = [];
this.messages = [];
if (query.startsWith('sql:')) {
this.messages = [];
try {
try {
if (query.startsWith('sql:')) {
this.messages = [];
this.results = await tfrpc.rpc.query(
query.substring('sql:'.length),
[]
);
} catch (e) {
this.results = [];
this.error = e;
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
} catch (e) {
this.messages = [];
this.results = [];
this.error = e;
console.log(e);
}
}
@@ -133,17 +134,25 @@ class TfTabSearchElement extends LitElement {
}
}
render() {
async query_results() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
this._query = this.search(this.query);
}
let self = this;
await this._query;
}
render() {
return html`
<style>
${generate_theme()}
</style>
<div class="w3-padding">${this.render_results()}</div>
<div class="w3-padding">
${until(
this.query_results().then(this.render_results.bind(this)),
html`<p>Searching...<span class="w3-animate-fading">🦀</span></p>`
)}
</div>
`;
}
}

View File

@@ -39,7 +39,9 @@ class TfUserElement extends LitElement {
name = this.icon_only
? undefined
: !this.nolink
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
? html`<a target="_top" href=${'#' + encodeURIComponent(this.id)}
>${name_string}</a
>`
: html`<span>${name_string}</span>`;
if (user) {

View File

@@ -1474,48 +1474,7 @@ function blur() {
* @param event The message.
*/
function message(event) {
if (
event.data &&
event.data.event == 'resizeMe' &&
event.data.width &&
event.data.height
) {
let iframe = document.getElementById('iframe_' + event.data.name);
iframe.setAttribute('width', event.data.width);
iframe.setAttribute('height', event.data.height);
} else if (event.data && event.data.action == 'setHash') {
window.location.hash = event.data.hash;
} else if (event.data && event.data.action == 'storeBlob') {
fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/binary',
},
body: event.data.blob.buffer,
})
.then(function (response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.text();
})
.then(function (text) {
let iframe = document.getElementById('document');
iframe.contentWindow.postMessage(
{
storeBlobComplete: {
name: event.data.blob.name,
path: text,
type: event.data.blob.type,
context: event.data.context,
},
},
'*'
);
});
} else {
send({event: 'message', message: event.data});
}
send({event: 'message', message: event.data});
}
/**

View File

@@ -235,8 +235,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
if (!options?.script || options?.script === 'app.js') {
process.app = new App();
}
process.lastActive = Date.now();
process.lastPing = null;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@@ -515,21 +513,6 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
);
}
};
if (process.credentials?.permissions?.administration) {
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.swapWithServerIdentity(
process.credentials.session.name,
id
);
}
};
}
if (
process.credentials &&
process.credentials.session &&

File diff suppressed because one or more lines are too long

View File

@@ -186,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.8",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"version": "6.39.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.3.tgz",
"integrity": "sha512-ZR32LYnPMpf7XZcrYJpSrHJUHNZPTj73/amTtZLhAwzYhSKiDI2OZmCiXbTRvxL1T8X7QTHnCG+KfnRJvH/QsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -307,9 +307,9 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.4.tgz",
"integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"

View File

@@ -97,40 +97,37 @@ static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_d
tf_free(work);
}
static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
JSValue name_value = JS_IsObject(session) ? JS_GetPropertyStr(context, session, "name") : JS_UNDEFINED;
const char* result = JS_IsString(name_value) ? JS_ToCString(context, name_value) : NULL;
JS_FreeValue(context, name_value);
JS_FreeValue(context, session);
JS_FreeValue(context, credentials);
return result;
}
static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue user = argv[0];
JSValue process = data[0];
const char* user_string = JS_IsString(user) ? JS_ToCString(context, user) : NULL;
const char* session_name_string = _tf_ssb_get_process_credentials_session_name(context, process);
if (JS_IsObject(process))
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
if (JS_IsObject(credentials))
{
JSValue session = JS_GetPropertyStr(context, credentials, "session");
if (JS_IsObject(session))
{
JSValue session_name = JS_GetPropertyStr(context, session, "name");
const char* session_name_string = JS_IsString(session_name) ? JS_ToCString(context, session_name) : NULL;
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
JS_FreeValue(context, session_name);
}
JS_FreeValue(context, session);
}
JS_FreeValue(context, credentials);
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
if (user_string)
{
@@ -380,26 +377,12 @@ static void _tf_api_core_permissions_granted_after_work(tf_ssb_t* ssb, int statu
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->user);
JS_FreeCString(context, work->user);
tf_free((void*)work->package_owner);
tf_free((void*)work->package_name);
tf_free(work);
}
static const char* _tf_ssb_get_process_credentials_session_name(JSContext* context, JSValue process)
{
JSValue credentials = JS_IsObject(process) ? JS_GetPropertyStr(context, process, "credentials") : JS_UNDEFINED;
JSValue session = JS_IsObject(credentials) ? JS_GetPropertyStr(context, credentials, "session") : JS_UNDEFINED;
JSValue name_value = JS_IsObject(session) ? JS_GetPropertyStr(context, session, "name") : JS_UNDEFINED;
const char* name = JS_IsString(name_value) ? JS_ToCString(context, name_value) : NULL;
const char* result = tf_strdup(name);
JS_FreeCString(context, name);
JS_FreeValue(context, name_value);
JS_FreeValue(context, session);
JS_FreeValue(context, credentials);
return result;
}
static JSValue _tf_api_core_permissionsGranted(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
@@ -508,7 +491,7 @@ static JSValue _tf_ssb_getActiveIdentity(JSContext* context, JSValueConst this_v
.package_name = tf_strdup(package_name),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_free((void*)name);
JS_FreeCString(context, name);
JS_FreeCString(context, package_owner);
JS_FreeCString(context, package_name);
@@ -611,7 +594,7 @@ static JSValue _tf_ssb_getIdentities(JSContext* context, JSValueConst this_val,
.context = context,
};
memcpy(work->user, user, user_length + 1);
tf_free((void*)user);
JS_FreeCString(context, user);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_identities_work, _tf_ssb_get_identities_after_work, work);
@@ -838,7 +821,7 @@ static void _tf_ssb_modify_block_after_work(tf_ssb_t* ssb, int status, void* use
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
JS_FreeValue(context, request->result);
tf_free((void*)request->user);
JS_FreeCString(context, request->user);
tf_free(request);
}
@@ -1039,7 +1022,6 @@ typedef struct _global_setting_set_t
static void _tf_ssb_globalSettingsSet_work(tf_ssb_t* ssb, void* user_data)
{
global_setting_set_t* work = user_data;
tf_printf("SET [%s]=[%s]\n", work->key, work->value);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
work->done = tf_ssb_db_set_global_setting_from_string(db, work->key, work->value);
tf_ssb_release_db_writer(ssb, db);
@@ -1159,6 +1141,84 @@ static JSValue _tf_ssb_delete_user(JSContext* context, JSValueConst this_val, in
return result;
}
typedef struct _swap_with_server_identity_t
{
char server_id[k_id_base64_len];
char user_id[k_id_base64_len];
JSValue promise[2];
char* error;
char user[];
} swap_with_server_identity_t;
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
swap_with_server_identity_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
work->error = tf_ssb_db_swap_with_server_identity(db, work->user, work->user_id, work->server_id);
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
swap_with_server_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
JSValue exception = JS_GetException(context);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
tf_free(work->error);
JS_FreeValue(context, exception);
JS_FreeValue(context, arg);
}
else
{
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
}
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_swap_with_server_identity_permission_callback(JSContext* context, bool granted, JSValue value, void* user_data)
{
swap_with_server_identity_t* work = user_data;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
}
static JSValue _tf_ssb_swap_with_server_identity(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
const char* user = _tf_ssb_get_process_credentials_session_name(context, data[0]);
size_t user_length = user ? strlen(user) : 0;
const char* id = JS_ToCString(context, argv[0]);
swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
*work = (swap_with_server_identity_t) { 0 };
tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
tf_string_set(work->user_id, sizeof(work->user_id), id);
if (user)
{
memcpy(work->user, user, user_length + 1);
}
else
{
*work->user = '\0';
}
JSValue result = JS_NewPromiseCapability(context, work->promise);
char description[1024];
snprintf(description, sizeof(description), "Swap identity %s with %s.", work->user_id, work->server_id);
_tf_ssb_permission_test(context, data[0], "delete_user", description, _tf_ssb_swap_with_server_identity_permission_callback, work);
JS_FreeCString(context, id);
JS_FreeCString(context, user);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
@@ -1201,6 +1261,8 @@ static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_va
JS_SetPropertyStr(context, ssb, "addBlock", JS_NewCFunctionData(context, _tf_ssb_add_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "removeBlock", JS_NewCFunctionData(context, _tf_ssb_remove_block, 1, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "getBlocks", JS_NewCFunctionData(context, _tf_ssb_get_blocks, 0, 0, 1, &process));
JS_SetPropertyStr(context, ssb, "swapWithServerIdentity", JS_NewCFunctionData(context, _tf_ssb_swap_with_server_identity, 1, 0, 1, &process));
}
JS_FreeValue(context, administration);
JS_FreeValue(context, permissions);

View File

@@ -367,10 +367,10 @@ static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this
JSValue process_app = JS_GetPropertyStr(context, app->process, "app");
JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
JS_SetPropertyStr(context, process_app, "_on_output", on_output);
JS_FreeValue(context, process_app);
JSValue send = JS_GetPropertyStr(context, process_app, "send");
JSValue result = JS_Call(context, send, process_app, 0, NULL);
JS_FreeValue(context, process_app);
JS_FreeValue(context, send);
tf_util_report_error(context, result);
JS_FreeValue(context, result);

View File

@@ -19,7 +19,7 @@
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>27</string>
<string>49</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>LSRequiresIPhoneOS</key>

View File

@@ -314,7 +314,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.rowid, "
"old.content); END");
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%ltrim%'"))
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%INSTR%'"))
{
tf_printf("Deleting incorrect messages_refs...\n");
_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS messages_refs");
@@ -337,7 +337,8 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"(j.value LIKE '#%' AND INSTR(j.value, ' ') = 0 AND INSTR(j.value, char(9)) = 0 AND INSTR(j.value, char(10)) = 0 AND INSTR(j.value, char(13)) = 0 AND INSTR(j.value, "
"',') = 0) "
"ON CONFLICT DO NOTHING");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
tf_printf("Done.\n");
@@ -351,7 +352,8 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"(j.value LIKE '#%' AND INSTR(j.value, ' ') = 0 AND INSTR(j.value, char(9)) = 0 AND INSTR(j.value, char(10)) = 0 AND INSTR(j.value, char(13)) = 0 AND INSTR(j.value, ',') "
"= 0) "
"ON CONFLICT DO NOTHING; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_refs");
_tf_ssb_db_exec(db, "CREATE TRIGGER IF NOT EXISTS messages_ad_refs AFTER DELETE ON messages BEGIN DELETE FROM messages_refs WHERE messages_refs.message = old.id; END");
@@ -2953,3 +2955,52 @@ void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double t
sqlite3_finalize(statement);
}
}
char* tf_ssb_db_swap_with_server_identity(sqlite3* db, const char* user, const char* user_id, const char* server_id)
{
tf_printf("SWAP user=%s user_id=%s server_id=%s\n", user, user_id, server_id);
char* result = NULL;
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "UPDATE identities SET user = ? WHERE user = ? AND '@' || public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, server_id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1 &&
sqlite3_reset(statement) == SQLITE_OK && sqlite3_bind_text(statement, 1, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, user_id, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1)
{
char* commit_error = NULL;
if (sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, &commit_error) != SQLITE_OK)
{
result = commit_error ? tf_strdup(commit_error) : tf_strdup(sqlite3_errmsg(db));
}
if (commit_error)
{
sqlite3_free(commit_error);
}
}
else
{
result = tf_strdup(sqlite3_errmsg(db) ? sqlite3_errmsg(db) : "swap failed");
}
sqlite3_finalize(statement);
}
else
{
result = tf_strdup(sqlite3_errmsg(db));
}
}
else
{
result = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
return result;
}

View File

@@ -647,4 +647,14 @@ bool tf_ssb_db_is_blocked(sqlite3* db, const char* id);
*/
void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double timestamp, void* user_data), void* user_data);
/**
** Swap a user's identity with the server identity.
** @param db The database.
** @param user The user.
** @param user_id The user identity.
** @param server_id The server identity.
** @return Null on success or an error message on error. Free with tf_free().
*/
char* tf_ssb_db_swap_with_server_identity(sqlite3* db, const char* user, const char* user_id, const char* server_id);
/** @} */

View File

@@ -252,117 +252,6 @@ static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val,
return result;
}
typedef struct _swap_with_server_identity_t
{
char server_id[k_id_base64_len];
char id[k_id_base64_len];
JSValue promise[2];
char* error;
char user[];
} swap_with_server_identity_t;
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
swap_with_server_identity_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (tf_ssb_db_user_has_permission(ssb, db, work->user, "administration"))
{
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "UPDATE identities SET user = ? WHERE user = ? AND '@' || public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->server_id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1 &&
sqlite3_reset(statement) == SQLITE_OK && sqlite3_bind_text(statement, 1, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->id, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1)
{
char* commit_error = NULL;
if (sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, &commit_error) != SQLITE_OK)
{
work->error = commit_error ? tf_strdup(commit_error) : tf_strdup(sqlite3_errmsg(db));
}
if (commit_error)
{
sqlite3_free(commit_error);
}
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db) ? sqlite3_errmsg(db) : "swap failed");
}
sqlite3_finalize(statement);
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db));
}
}
else
{
work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
}
else
{
work->error = tf_strdup("not administrator");
}
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
swap_with_server_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
JSValue exception = JS_GetException(context);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
tf_free(work->error);
JS_FreeValue(context, exception);
JS_FreeValue(context, arg);
}
else
{
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
}
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_swap_with_server_identity(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]);
swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
*work = (swap_with_server_identity_t) { 0 };
tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
tf_string_set(work->id, sizeof(work->id), id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
}
return result;
}
typedef struct _get_private_key_t
{
JSContext* context;
@@ -2196,7 +2085,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
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, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));
JS_SetPropertyStr(context, object, "privateMessageEncrypt", JS_NewCFunction(context, _tf_ssb_private_message_encrypt, "privateMessageEncrypt", 4));
JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));