20 Commits

Author SHA1 Message Date
fb6e554e59 ssb: Respect blocks when getting blocks and accounts at the db level.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m59s
2025-11-29 12:14:34 -05:00
d200e361f7 docs: Mention flagging in the iOS agreement. 2025-11-29 11:47:59 -05:00
bc3fd57d7a ssb: The block list can be crudely managed through the admin app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m54s
2025-11-29 11:33:16 -05:00
fa4ef3b082 ssb: Show flags on more message type. 2025-11-29 10:51:00 -05:00
0827718d68 ssb: Un-clobber the flagged message UI. Whoops. 2025-11-29 10:41:08 -05:00
0ec862eaac ssb: Fight blog post CSS a bit more.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 12m32s
2025-11-29 10:28:10 -05:00
7e1621dfb4 ssb: Slight improvements to blog header display. 2025-11-29 10:17:13 -05:00
c4d4e3822d update: CodeMirror. 2025-11-29 10:01:20 -05:00
d2e5015eac test: Wait for alerts harder. 2025-11-29 10:01:12 -05:00
510c2f81bd update: sqlite 3.51.1. 2025-11-28 18:42:48 -05:00
4f2e0245d3 ssb: Make blocks begin to do something.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m21s
2025-11-27 16:43:15 -05:00
eecdbf6852 ssb: Exercise at least calling adding/removing blocks in a test.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m12s
2025-11-27 16:06:33 -05:00
ddc4603f13 ssb: Add some plausible API and a table for storing instance-wide blocks. 2025-11-27 14:33:57 -05:00
759b522cd1 ssb: Preliminary view of flagged messages. Seems a bit counter-productive, but here we are.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-11-27 13:32:37 -05:00
7ecb4a192d buttfeed: Add SoapDog's PatchWork.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 11m18s
2025-11-27 13:05:19 -05:00
d84626ac31 ssb: Fix showing flags if we see the messages in the other order.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-26 12:16:23 -05:00
9c36e0db7b ssb: Show flagged messages similar to a message with a content warning.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m19s
2025-11-25 19:20:49 -05:00
fcd26bac1c ssb: Add some UI for posting 'flag' messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m8s
2025-11-25 19:04:52 -05:00
e8e7c98705 build: wip.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m14s
2025-11-25 18:31:25 -05:00
b5af5cc223 build: Let's start work on the December build. 2025-11-25 18:29:50 -05:00
32 changed files with 628 additions and 104 deletions

View File

@@ -16,14 +16,14 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 48
VERSION_CODE_IOS := 26
VERSION_NUMBER := 0.2025.11
VERSION_CODE := 49
VERSION_CODE_IOS := 27
VERSION_NUMBER := 0.2025.12-wip
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.5
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3510000.zip
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3510100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.18.2/bundletool-all-1.18.2.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🎛",
"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256"
"previous": "&bRhS1LQIH8WQjbBfQqdhjLv7tqDdHT7IEPyCmj39b+4=.sha256"
}

View File

@@ -8,12 +8,20 @@ tfrpc.register(function global_settings_set(key, value) {
return core.globalSettingsSet(key, value);
});
tfrpc.register(function addBlock(id) {
return ssb.addBlock(id);
});
tfrpc.register(function removeBlock(id) {
return ssb.removeBlock(id);
});
async function main() {
try {
let data = {
users: {},
granted: await core.allPermissionsGranted(),
settings: await core.globalSettingsDescriptions(),
blocks: await ssb.getBlocks(),
};
for (let user of await core.users()) {
data.users[user] = await core.permissionsForUser(user);

View File

@@ -16,6 +16,14 @@ function delete_user(user) {
}
}
async function add_block() {
await tfrpc.rpc.addBlock(document.getElementById('add_block').value);
}
async function remove_block(id) {
await tfrpc.rpc.removeBlock(id);
}
function global_settings_set(key, value) {
tfrpc.rpc
.global_settings_set(key, value)
@@ -94,11 +102,32 @@ ${description.value}</textarea
${user}: ${permissions.map((x) => permission_template(x))}
</li>
`;
const block_template = (block) => html`
<li class="w3-card w3-margin">
<button
class="w3-button w3-theme-action"
@click=${(e) => remove_block(block.id)}
>
Delete
</button>
<code>${block.id}</code>
${new Date(block.timestamp)}
</li>
`;
const users_template = (users) =>
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
<ul class="w3-ul">
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`;
const blocks_template = (blocks) =>
html` <header class="w3-container w3-theme-l2"><h2>Blocks</h2></header>
<div class="w3-row w3-margin">
<input type="text" class="w3-threequarter w3-input" id="add_block"></input>
<button class="w3-quarter w3-button w3-theme-action" @click=${add_block}>Add</button>
</div>
<ul class="w3-ul">
${blocks.map((b) => block_template(b))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
@@ -109,7 +138,7 @@ ${description.value}</textarea
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div>
${users_template(data.users)}
${users_template(data.users)} ${blocks_template(data.blocks)}
</div> `;
render(page_template(g_data), document.body);
});

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&7dPNAI4sffljUTiwGr3XEUeB8sBD72CFkWMk/o0Z2pw=.sha256"
"previous": "&E7oElXjP2g+Xb8dhrRyTVdv8EJArjJRvgmfl1prtciw=.sha256"
}

View File

@@ -185,6 +185,7 @@ class TfElement extends LitElement {
'',
'@',
'👍',
'🚩',
...Object.keys(this.visible_private())
.sort()
.map((x) => '🔐' + JSON.parse(x).join(',')),
@@ -491,6 +492,13 @@ class TfElement extends LitElement {
`,
k_args
),
tfrpc.rpc.query(
`
SELECT '🚩' AS channel, MAX(messages.rowid) AS rowid FROM messages
WHERE messages.content ->> 'type' = 'flag'
`,
k_args
),
])
).flat();
let latest = {'🔐': undefined};

View File

@@ -196,6 +196,26 @@ class TfMessageElement extends LitElement {
);
}
flag(event) {
let reason = prompt(
'What is the reason for reporting this content (spam, nsfw, ...)?',
'offensive'
);
if (reason !== undefined) {
tfrpc.rpc
.appendMessage(this.whoami, {
type: 'flag',
flag: {
link: this.message.id,
reason: reason.length ? reason : undefined,
},
})
.catch(function (error) {
alert(error?.message);
});
}
}
show_image(link) {
let div = document.createElement('div');
div.style.left = 0;
@@ -505,6 +525,12 @@ class TfMessageElement extends LitElement {
>
👍 React
</button>
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.flag}
>
⚠️ Flag
</button>
${formats.map(
([format, name]) => html`
<button
@@ -575,9 +601,11 @@ class TfMessageElement extends LitElement {
let self = this;
return this.render_frame(html`
${self.render_header()}
${self.format == 'raw'
? html`<div class="w3-container">${self.render_raw()}</div>`
: inner}
<div class="w3-container">
${self.format == 'raw'
? html`${self.render_raw()}`
: self.render_flagged(inner)}
</div>
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
@@ -703,6 +731,38 @@ class TfMessageElement extends LitElement {
: undefined;
}
render_flagged(inner) {
if (this.message.flags) {
return html`
<div
class="w3-panel w3-round-xlarge w3-theme-l4 w3"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>
${this.message.flags
? html`<p>
Caution: This message has been flagged
${this.message.flags.length}
time${this.message.flags.length == 1 ? '' : 's'}.
</p>`
: undefined}
</p>
<p class="w3-small">
${inner !== undefined
? this.is_expanded(':cw')
? 'Show less'
: 'Show more'
: undefined}
</p>
</div>
${this.is_expanded(':cw') ? inner : undefined}
`;
} else {
return inner;
}
}
_render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
@@ -850,6 +910,7 @@ class TfMessageElement extends LitElement {
</div>
</div>
</div>
<div class="w3-container">${this.render_flagged(undefined)}</div>
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(
(x) => html`
@@ -965,7 +1026,19 @@ class TfMessageElement extends LitElement {
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
<p>
${this.message.flags
? html`<p>
Caution: This message has been flagged
${this.message.flags.length}
time${this.message.flags.length == 1 ? '' : 's'}.
</p>`
: undefined}
${content.contentWarning
? html`<p>${content.contentWarning}</p>`
: undefined}
</p>
<p class="w3-small">
${this.is_expanded(':cw') ? 'Show less' : 'Show more'}
</p>
@@ -976,11 +1049,12 @@ class TfMessageElement extends LitElement {
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload = content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
let payload =
this.message.flags || content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${payload}</div>
@@ -999,15 +1073,13 @@ class TfMessageElement extends LitElement {
`);
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) {
self.blog_data = data;
self.blog_data = tfrpc.rpc.get_blob(content.blog).then(function (data) {
return data
? unsafeHTML(tfutils.markdown(data))
: html`Blog post content unavailable.`;
});
let payload = this.expanded[(this.message.id || '') + ':blog']
? html`<div>
${this.blog_data
? unsafeHTML(tfutils.markdown(this.blog_data))
: 'Loading...'}
</div>`
? until(this.blog_data, 'Loading...')
: undefined;
let body;
switch (this.format) {
@@ -1020,15 +1092,24 @@ class TfMessageElement extends LitElement {
case 'message':
body = html`
<div
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
@click=${(x) => self.toggle_expanded(':blog')}>
class="w3-border w3-theme-l4 w3-round-xlarge"
style="padding: 8px; margin: 4px; cursor: pointer"
@click=${(x) => self.toggle_expanded(':blog')}
>
<h2>${content.title}</h2>
<div style="display: flex; flex-direction: row">
<img src=/${content.thumbnail}/view></img>
<div style="display: flex; flex-direction: row; gap: 8px">
${content.thumbnail
? html`<img src=/${content.thumbnail}/view style="max-width: 25vw; max-height: 25vw"></img>`
: undefined}
<span>${content.summary}</span>
</div>
<p class="w3-small">
${this.expanded[(this.message.id || '') + ':blog']
? 'Show less'
: 'Show more'}
</p>
</div>
${payload}
<div class="w3-container">${payload}</div>
`;
break;
}

View File

@@ -66,6 +66,16 @@ class TfNewsElement extends LitElement {
}
parent.votes.push(message);
message.parent_message = message.content.vote.link;
} else if (message.content.type == 'flag') {
let parent = ensure_message(message.content.flag.link, message.rowid);
if (!parent.flags) {
parent.flags = [];
}
parent.flags.push(message);
parent.flags = Object.values(
Object.fromEntries(parent.flags.map((x) => [x.id, x]))
);
message.parent_message = message.content.flag.link;
} else if (message.content.type == 'post') {
if (message.content.root) {
if (typeof message.content.root === 'string') {
@@ -106,6 +116,7 @@ class TfNewsElement extends LitElement {
message.parent_message = placeholder.parent_message;
message.child_messages = placeholder.child_messages;
message.votes = placeholder.votes;
message.flags = placeholder.flags;
if (
placeholder.parent_message &&
messages_by_id[placeholder.parent_message]

View File

@@ -84,7 +84,6 @@ class TfTabNewsFeedElement extends LitElement {
`,
[JSON.stringify(combined.map((x) => x.id))]
);
let t0 = new Date();
let result = [].concat(
combined,
await tfrpc.rpc.query(
@@ -101,8 +100,7 @@ class TfTabNewsFeedElement extends LitElement {
]
)
);
let t1 = new Date();
console.log((t1 - t0) / 1000);
console.log(result);
return result;
}
@@ -227,7 +225,8 @@ class TfTabNewsFeedElement extends LitElement {
k_max_results,
]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
let decrypted = (await this.decrypt(result)).filter((x) => x.decrypted);
result = await this._fetch_related_messages(decrypted);
} else if (this.hash == '#👍') {
result = await tfrpc.rpc.query(
`
@@ -246,6 +245,23 @@ class TfTabNewsFeedElement extends LitElement {
`,
[JSON.stringify(this.following), start_time, end_time, k_max_results]
);
} else if (this.hash == '#🚩') {
result = await tfrpc.rpc.query(
`
WITH flags AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
WHERE
messages.content ->> 'type' = 'flag' AND
(?1 IS NULL OR messages.timestamp >= ?1) AND messages.timestamp < ?2
ORDER BY timestamp DESC limit ?3)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM flags
JOIN messages ON messages.id = flags.content ->> '$.flag.link'
UNION
SELECT TRUE AS is_primary, * FROM flags
`,
[start_time, end_time, k_max_results]
);
} else {
let initial_messages = await tfrpc.rpc.query(
`

View File

@@ -243,6 +243,12 @@ class TfTabNewsElement extends LitElement {
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a
>
<a
href="#🚩"
class="w3-bar-item w3-button"
style=${this.hash == '#🚩' ? 'font-weight: bold' : undefined}
>${this.unread_status('🚩')}🚩flagged</a
>
${Object.keys(this?.visible_private_messages ?? [])
?.sort()
?.map(

View File

@@ -494,18 +494,29 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
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?.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
);
}
};
imports.ssb.addBlock = async function (id) {
await imports.core.permissionTest('modify_blocks', `Block ${id}.`);
await ssb_internal.addBlock(id);
};
imports.ssb.removeBlock = async function (id) {
await imports.core.permissionTest('modify_blocks', `Unblock ${id}.`);
await ssb_internal.removeBlock(id);
};
imports.ssb.getBlocks = ssb_internal.getBlocks.bind(ssb_internal);
}
if (
process.credentials &&

View File

@@ -34,12 +34,15 @@
<p>
If you encounter objectionable content, you can filter it from your view
by blocking the user who posted it. This also makes it so that users
following you will not see it as a consequence of following you.
following you will not see it as a consequence of following you. You can
also flag a message to signal to operators of services to which you
connect that the content should be considered for blocking.
</p>
<p>
The <code>admin</code> app contains a variety of settings that control the
types of connections Tilde Friends will make or accept, including whether
the app will even accept or make connections at all.
the app will even accept or make connections at all. This is also where a
local blocklist can be managed.
</p>
<h2>This app is not a service</h2>
<p>
@@ -47,12 +50,6 @@
has no more ability to see or filter what you post or read than any other
user of the network.
</p>
<p>
If you believe objectionable content obtained from a service that is
running as part of the Secure Scuttlebutt network should be flagged,
report it to the operator of the service, generally by contacting the
email address listed when visiting the server address in a web browser.
</p>
<h2>Agreement</h2>
<p>
If you do not accept these terms, do not use this app. You may close and

View File

@@ -25,14 +25,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.2025.9";
version = "0.2025.11";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-1nhsfhdOO5HIiiTMb+uROB8nDPL/UpOYm52hZ/OpPyk=";
hash = "sha256-z4v4ghKOBTMv+agTUKg+HU8zfE4imluXFsozQCT4qX8=";
fetchSubmodules = true;
};

File diff suppressed because one or more lines are too long

View File

@@ -217,9 +217,9 @@
}
},
"node_modules/@lezer/common": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz",
"integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==",
"license": "MIT"
},
"node_modules/@lezer/css": {
@@ -276,9 +276,9 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.3.tgz",
"integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==",
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.4.tgz",
"integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"

2
deps/sqlite/shell.c vendored
View File

@@ -14944,6 +14944,7 @@ static char *intckMprintf(sqlite3_intck *p, const char *zFmt, ...){
sqlite3_free(zRet);
zRet = 0;
}
va_end(ap);
return zRet;
}
@@ -29053,6 +29054,7 @@ static int do_meta_command(char *zLine, ShellState *p){
}
p->showHeader = savedShowHeader;
p->shellFlgs = savedShellFlags;
rc = p->nErr>0;
}else
if( c=='e' && cli_strncmp(azArg[0], "echo", n)==0 ){

58
deps/sqlite/sqlite3.c vendored
View File

@@ -1,6 +1,6 @@
/******************************************************************************
** This file is an amalgamation of many separate C source files from SQLite
** version 3.51.0. By combining all the individual C code files into this
** version 3.51.1. By combining all the individual C code files into this
** single large file, the entire code can be compiled as a single translation
** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements
@@ -18,7 +18,7 @@
** separate file. This file contains only code for the core SQLite library.
**
** The content in this amalgamation comes from Fossil check-in
** fb2c931ae597f8d00a37574ff67aeed3eced with changes in files:
** 281fc0e9afc38674b9b0991943b9e9d1e64c with changes in files:
**
**
*/
@@ -467,12 +467,12 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.51.0"
#define SQLITE_VERSION_NUMBER 3051000
#define SQLITE_SOURCE_ID "2025-11-04 19:38:17 fb2c931ae597f8d00a37574ff67aeed3eced4e5547f9120744ae4bfa8e74527b"
#define SQLITE_SCM_BRANCH "trunk"
#define SQLITE_SCM_TAGS "release major-release version-3.51.0"
#define SQLITE_SCM_DATETIME "2025-11-04T19:38:17.314Z"
#define SQLITE_VERSION "3.51.1"
#define SQLITE_VERSION_NUMBER 3051001
#define SQLITE_SOURCE_ID "2025-11-28 17:28:25 281fc0e9afc38674b9b0991943b9e9d1e64c6cbdb133d35f6f5c87ff6af38a88"
#define SQLITE_SCM_BRANCH "branch-3.51"
#define SQLITE_SCM_TAGS "release version-3.51.1"
#define SQLITE_SCM_DATETIME "2025-11-28T17:28:25.933Z"
/*
** CAPI3REF: Run-Time Library Version Numbers
@@ -10747,7 +10747,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** &nbsp; ){
** &nbsp; // do something with pVal
** &nbsp; }
** &nbsp; if( rc!=SQLITE_OK ){
** &nbsp; if( rc!=SQLITE_DONE ){
** &nbsp; // an error has occurred
** &nbsp; }
** </pre></blockquote>)^
@@ -38004,6 +38004,7 @@ SQLITE_PRIVATE void *sqlite3HashInsert(Hash *pH, const char *pKey, void *data){
return 0;
}
/************** End of hash.c ************************************************/
/************** Begin file opcodes.c *****************************************/
/* Automatically generated. Do not edit */
@@ -130655,6 +130656,7 @@ SQLITE_PRIVATE void sqlite3SchemaClear(void *p){
for(pElem=sqliteHashFirst(&temp2); pElem; pElem=sqliteHashNext(pElem)){
sqlite3DeleteTrigger(&xdb, (Trigger*)sqliteHashData(pElem));
}
sqlite3HashClear(&temp2);
sqlite3HashInit(&pSchema->tblHash);
for(pElem=sqliteHashFirst(&temp1); pElem; pElem=sqliteHashNext(pElem)){
@@ -160976,9 +160978,12 @@ SQLITE_PRIVATE int sqlite3VtabEponymousTableInit(Parse *pParse, Module *pMod){
addModuleArgument(pParse, pTab, sqlite3DbStrDup(db, pTab->zName));
addModuleArgument(pParse, pTab, 0);
addModuleArgument(pParse, pTab, sqlite3DbStrDup(db, pTab->zName));
db->nSchemaLock++;
rc = vtabCallConstructor(db, pTab, pMod, pModule->xConnect, &zErr);
db->nSchemaLock--;
if( rc ){
sqlite3ErrorMsg(pParse, "%s", zErr);
pParse->rc = rc;
sqlite3DbFree(db, zErr);
sqlite3VtabEponymousTableClear(db, pMod);
}
@@ -174040,8 +174045,22 @@ SQLITE_PRIVATE void sqlite3WhereEnd(WhereInfo *pWInfo){
sqlite3VdbeAddOp2(v, OP_Goto, 1, pLevel->p2);
}
#endif /* SQLITE_DISABLE_SKIPAHEAD_DISTINCT */
if( pTabList->a[pLevel->iFrom].fg.fromExists ){
sqlite3VdbeAddOp2(v, OP_Goto, 0, sqlite3VdbeCurrentAddr(v)+2);
if( pTabList->a[pLevel->iFrom].fg.fromExists && i==pWInfo->nLevel-1 ){
/* If the EXISTS-to-JOIN optimization was applied, then the EXISTS
** loop(s) will be the inner-most loops of the join. There might be
** multiple EXISTS loops, but they will all be nested, and the join
** order will not have been changed by the query planner. If the
** inner-most EXISTS loop sees a single successful row, it should
** break out of *all* EXISTS loops. But only the inner-most of the
** nested EXISTS loops should do this breakout. */
int nOuter = 0; /* Nr of outer EXISTS that this one is nested within */
while( nOuter<i ){
if( !pTabList->a[pLevel[-nOuter-1].iFrom].fg.fromExists ) break;
nOuter++;
}
testcase( nOuter>0 );
sqlite3VdbeAddOp2(v, OP_Goto, 0, pLevel[-nOuter].addrBrk);
VdbeComment((v, "EXISTS break"));
}
/* The common case: Advance to the next row */
if( pLevel->addrCont ) sqlite3VdbeResolveLabel(v, pLevel->addrCont);
@@ -186225,6 +186244,7 @@ SQLITE_PRIVATE void sqlite3LeaveMutexAndCloseZombie(sqlite3 *db){
/* Clear the TEMP schema separately and last */
if( db->aDb[1].pSchema ){
sqlite3SchemaClear(db->aDb[1].pSchema);
assert( db->aDb[1].pSchema->trigHash.count==0 );
}
sqlite3VtabUnlockList(db);
@@ -187553,7 +187573,7 @@ SQLITE_API const char *sqlite3_errmsg(sqlite3 *db){
*/
SQLITE_API int sqlite3_set_errmsg(sqlite3 *db, int errcode, const char *zMsg){
int rc = SQLITE_OK;
if( !sqlite3SafetyCheckSickOrOk(db) ){
if( !sqlite3SafetyCheckOk(db) ){
return SQLITE_MISUSE_BKPT;
}
sqlite3_mutex_enter(db->mutex);
@@ -249220,6 +249240,7 @@ static void fts5SegIterReverseInitPage(Fts5Index *p, Fts5SegIter *pIter){
while( 1 ){
u64 iDelta = 0;
if( i>=n ) break;
if( eDetail==FTS5_DETAIL_NONE ){
/* todo */
if( i<n && a[i]==0 ){
@@ -260283,7 +260304,7 @@ static void fts5SourceIdFunc(
){
assert( nArg==0 );
UNUSED_PARAM2(nArg, apUnused);
sqlite3_result_text(pCtx, "fts5: 2025-11-04 19:38:17 fb2c931ae597f8d00a37574ff67aeed3eced4e5547f9120744ae4bfa8e74527b", -1, SQLITE_TRANSIENT);
sqlite3_result_text(pCtx, "fts5: 2025-11-28 17:28:25 281fc0e9afc38674b9b0991943b9e9d1e64c6cbdb133d35f6f5c87ff6af38a88", -1, SQLITE_TRANSIENT);
}
/*
@@ -265104,7 +265125,12 @@ static int fts5VocabOpenMethod(
return rc;
}
/*
** Restore cursor pCsr to the state it was in immediately after being
** created by the xOpen() method.
*/
static void fts5VocabResetCursor(Fts5VocabCursor *pCsr){
int nCol = pCsr->pFts5->pConfig->nCol;
pCsr->rowid = 0;
sqlite3Fts5IterClose(pCsr->pIter);
sqlite3Fts5StructureRelease(pCsr->pStruct);
@@ -265114,6 +265140,12 @@ static void fts5VocabResetCursor(Fts5VocabCursor *pCsr){
pCsr->nLeTerm = -1;
pCsr->zLeTerm = 0;
pCsr->bEof = 0;
pCsr->iCol = 0;
pCsr->iInstPos = 0;
pCsr->iInstOff = 0;
pCsr->colUsed = 0;
memset(pCsr->aCnt, 0, sizeof(i64)*nCol);
memset(pCsr->aDoc, 0, sizeof(i64)*nCol);
}
/*

14
deps/sqlite/sqlite3.h vendored
View File

@@ -146,12 +146,12 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.51.0"
#define SQLITE_VERSION_NUMBER 3051000
#define SQLITE_SOURCE_ID "2025-11-04 19:38:17 fb2c931ae597f8d00a37574ff67aeed3eced4e5547f9120744ae4bfa8e74527b"
#define SQLITE_SCM_BRANCH "trunk"
#define SQLITE_SCM_TAGS "release major-release version-3.51.0"
#define SQLITE_SCM_DATETIME "2025-11-04T19:38:17.314Z"
#define SQLITE_VERSION "3.51.1"
#define SQLITE_VERSION_NUMBER 3051001
#define SQLITE_SOURCE_ID "2025-11-28 17:28:25 281fc0e9afc38674b9b0991943b9e9d1e64c6cbdb133d35f6f5c87ff6af38a88"
#define SQLITE_SCM_BRANCH "branch-3.51"
#define SQLITE_SCM_TAGS "release version-3.51.1"
#define SQLITE_SCM_DATETIME "2025-11-28T17:28:25.933Z"
/*
** CAPI3REF: Run-Time Library Version Numbers
@@ -10426,7 +10426,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** &nbsp; ){
** &nbsp; // do something with pVal
** &nbsp; }
** &nbsp; if( rc!=SQLITE_OK ){
** &nbsp; if( rc!=SQLITE_DONE ){
** &nbsp; // an error has occurred
** &nbsp; }
** </pre></blockquote>)^

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1758589230,
"narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=",
"lastModified": 1763948260,
"narHash": "sha256-dY9qLD0H0zOUgU3vWacPY6Qc421BeQAfm8kBuBtPVE0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0",
"rev": "1c8ba8d3f7634acac4a2094eef7c32ad9106532c",
"type": "github"
},
"original": {

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends"
android:versionCode="48"
android:versionName="0.2025.11">
android:versionCode="49"
android:versionName="0.2025.12-wip">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application

View File

@@ -23,7 +23,7 @@
#define YELLOW "\e[1;33m"
#define RESET "\e[0m"
static const int k_eula_version = 1;
static const int k_eula_version = 2;
static JSClassID _httpd_request_class_id;

View File

@@ -13,13 +13,13 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.2025.11</string>
<string>0.2025.12</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>26</string>
<string>27</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>LSRequiresIPhoneOS</key>

View File

@@ -271,7 +271,6 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
")");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)");
_tf_ssb_db_exec(db, "DELETE FROM identities WHERE user = ':auth'");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS invites ("
" invite_public_key TEXT PRIMARY KEY,"
@@ -279,6 +278,11 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
" use_count INTEGER,"
" expires INTEGER"
")");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blocks ("
" id TEXT PRIMARY KEY,"
" timestamp REAL"
")");
bool populate_fts = false;
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
@@ -890,7 +894,7 @@ bool tf_ssb_db_blob_has(sqlite3* db, const char* id)
{
bool result = false;
sqlite3_stmt* statement;
const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1";
const char* query = "SELECT COUNT(*) FROM blobs LEFT OUTER JOIN blocks ON blobs.id = blocks.id WHERE blobs.id = ?1 AND blocks.id IS NULL";
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
@@ -907,7 +911,7 @@ bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_
bool result = false;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
const char* query = "SELECT content FROM blobs WHERE id = ?1";
const char* query = "SELECT content FROM blobs LEFT OUTER JOIN blocks ON blobs.id = blocks.id WHERE blobs.id = ?1 AND blocks.id IS NULL";
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
@@ -1165,7 +1169,8 @@ bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, i
if (out_message_id)
{
const char* query = "SELECT id, sequence FROM messages WHERE author = ?1 ORDER BY sequence DESC LIMIT 1";
const char* query = "SELECT messages.id, messages.sequence FROM messages LEFT OUTER JOIN blocks ON messages.id = blocks.id WHERE author = ?1 AND blocks.id IS NULL ORDER "
"BY messages.sequence DESC LIMIT 1";
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
@@ -1189,7 +1194,8 @@ bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, i
}
else
{
const char* query = "SELECT max_sequence FROM messages_stats WHERE author = ?1";
const char* query = "SELECT messages_stats.max_sequence FROM messages_stats LEFT OUTER JOIN blocks ON messages_stats.author = blocks.id WHERE messages_stats.author = ?1 "
"AND blocks.id IS NULL";
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
@@ -2868,3 +2874,80 @@ tf_ssb_identity_info_t* tf_ssb_db_get_identity_info(tf_ssb_t* ssb, const char* u
tf_free(info);
return copy;
}
void tf_ssb_db_add_block(sqlite3* db, const char* id)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "INSERT INTO blocks (id, timestamp) VALUES (?, unixepoch() * 1000) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) != SQLITE_DONE)
{
tf_printf("add block: %s\n", sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
if (sqlite3_prepare_v2(db,
"INSERT INTO blocks (id, timestamp) SELECT messages_refs.ref AS id, unixepoch() * 1000 AS timestamp FROM messages_refs WHERE messages_refs.message = ? ON CONFLICT DO "
"NOTHING",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) != SQLITE_DONE)
{
tf_printf("add block messages ref: %s\n", sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
}
void tf_ssb_db_remove_block(sqlite3* db, const char* id)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "DELETE FROM blocks WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) != SQLITE_DONE)
{
tf_printf("remove block: %s\n", sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
}
bool tf_ssb_db_is_blocked(sqlite3* db, const char* id)
{
bool is_blocked = false;
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "SELECT 1 FROM blocks WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
is_blocked = true;
}
}
sqlite3_finalize(statement);
}
return is_blocked;
}
void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double timestamp, void* user_data), void* user_data)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare_v2(db, "SELECT id, timestamp FROM blocks ORDER BY timestamp", -1, &statement, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
callback((const char*)sqlite3_column_text(statement, 0), sqlite3_column_double(statement, 1), user_data);
}
sqlite3_finalize(statement);
}
}

View File

@@ -617,4 +617,34 @@ tf_ssb_identity_info_t* tf_ssb_db_get_identity_info(tf_ssb_t* ssb, const char* u
*/
void tf_ssb_db_add_blob_wants(sqlite3* db, const char* id);
/**
** Add an instance-wide block.
** @param db The database.
** @param id The account, message, or blob ID to block.
*/
void tf_ssb_db_add_block(sqlite3* db, const char* id);
/**
** Remove an instance-wide block.
** @param db The database.
** @param id The account, message, or blob ID to unblock.
*/
void tf_ssb_db_remove_block(sqlite3* db, const char* id);
/**
** Check if an ID is blocked on this instance.
** @param db The database.
** @param id The account, message, or blob ID to check.
** @return true if the id is blocked.
*/
bool tf_ssb_db_is_blocked(sqlite3* db, const char* id);
/**
** Get block list.
** @param db The database.
** @param callback Called for each entry.
** @param user_data User data to be passed to the callback.
*/
void tf_ssb_db_get_blocks(sqlite3* db, void (*callback)(const char* id, double timestamp, void* user_data), void* user_data);
/** @} */

View File

@@ -2172,6 +2172,128 @@ static JSValue _tf_ssb_port(JSContext* context, JSValueConst this_val, int argc,
return JS_NewInt32(context, tf_ssb_server_get_port(ssb));
}
typedef struct _modify_block_t
{
char id[k_id_base64_len];
bool add;
JSValue promise[2];
} modify_block_t;
static void _tf_ssb_modify_block_work(tf_ssb_t* ssb, void* user_data)
{
modify_block_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (work->add)
{
tf_ssb_db_add_block(db, work->id);
}
else
{
tf_ssb_db_remove_block(db, work->id);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_modify_block_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
modify_block_t* request = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_Call(context, request->promise[0], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, request->promise[0]);
JS_FreeValue(context, request->promise[1]);
tf_free(request);
}
static JSValue _tf_ssb_add_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) { .add = true };
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
return result;
}
static JSValue _tf_ssb_remove_block(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* id = JS_ToCString(context, argv[0]);
modify_block_t* work = tf_malloc(sizeof(modify_block_t));
*work = (modify_block_t) { .add = false };
tf_string_set(work->id, sizeof(work->id), id);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JS_FreeCString(context, id);
tf_ssb_run_work(ssb, _tf_ssb_modify_block_work, _tf_ssb_modify_block_after_work, work);
return result;
}
typedef struct _block_t
{
char id[k_id_base64_len];
double timestamp;
} block_t;
typedef struct _get_blocks_t
{
block_t* blocks;
int count;
JSValue promise[2];
} get_blocks_t;
static void _get_blocks_callback(const char* id, double timestamp, void* user_data)
{
get_blocks_t* work = user_data;
work->blocks = tf_resize_vec(work->blocks, sizeof(block_t) * (work->count + 1));
work->blocks[work->count] = (block_t) { .timestamp = timestamp };
tf_string_set(work->blocks[work->count].id, sizeof(work->blocks[work->count].id), id);
work->count++;
}
static void _tf_ssb_get_blocks_work(tf_ssb_t* ssb, void* user_data)
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_blocks(db, _get_blocks_callback, user_data);
tf_ssb_release_db_reader(ssb, db);
}
static void _tf_ssb_get_blocks_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_blocks_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->count; i++)
{
JSValue entry = JS_NewObject(context);
JS_SetPropertyStr(context, entry, "id", JS_NewString(context, work->blocks[i].id));
JS_SetPropertyStr(context, entry, "timestamp", JS_NewFloat64(context, work->blocks[i].timestamp));
JS_SetPropertyUint32(context, result, i, entry);
}
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_get_blocks(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
get_blocks_t* work = tf_malloc(sizeof(get_blocks_t));
*work = (get_blocks_t) { 0 };
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_get_blocks_work, _tf_ssb_get_blocks_after_work, work);
return result;
}
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
{
JS_NewClassID(&_tf_ssb_classId);
@@ -2227,6 +2349,9 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object_internal, "getIdentityInfo", JS_NewCFunction(context, _tf_ssb_getIdentityInfo, "getIdentityInfo", 3));
JS_SetPropertyStr(context, object_internal, "addEventListener", JS_NewCFunction(context, _tf_ssb_add_event_listener, "addEventListener", 2));
JS_SetPropertyStr(context, object_internal, "removeEventListener", JS_NewCFunction(context, _tf_ssb_remove_event_listener, "removeEventListener", 2));
JS_SetPropertyStr(context, object_internal, "addBlock", JS_NewCFunction(context, _tf_ssb_add_block, "addBlock", 1));
JS_SetPropertyStr(context, object_internal, "removeBlock", JS_NewCFunction(context, _tf_ssb_remove_block, "removeBlock", 1));
JS_SetPropertyStr(context, object_internal, "getBlocks", JS_NewCFunction(context, _tf_ssb_get_blocks, "getBlocks", 0));
JS_FreeValue(context, global);
}

View File

@@ -155,7 +155,7 @@ static void _tf_ssb_rpc_blobs_has_work(tf_ssb_connection_t* connection, void* us
blobs_has_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
work->found = tf_ssb_db_blob_has(db, work->id);
work->found = tf_ssb_db_blob_has(db, work->id) && !tf_ssb_db_is_blocked(db, work->id);
tf_ssb_release_db_reader(ssb, db);
}

View File

@@ -1851,4 +1851,81 @@ void tf_ssb_test_following_perf(const tf_test_options_t* options)
uv_run(&loop, UV_RUN_DEFAULT);
uv_loop_close(&loop);
}
static void _store_callback(const char* id, bool verified, bool is_new, void* user_data)
{
tf_string_set(user_data, k_id_base64_len, id);
}
void tf_ssb_test_blocks(const tf_test_options_t* options)
{
uv_loop_t loop = { 0 };
uv_loop_init(&loop);
tf_printf("Testing blocks.\n");
unlink("out/test_db0.sqlite");
tf_ssb_t* ssb = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
tf_ssb_generate_keys(ssb);
uint8_t priv[512] = { 0 };
tf_ssb_get_private_key(ssb, priv, sizeof(priv));
char id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, id, sizeof(id));
tf_printf("ID %s\n", id);
char blob_id[k_id_base64_len] = { 0 };
const char* k_blob = "Hello, blob!";
bool blob_stored = false;
tf_ssb_add_blob_stored_callback(ssb, _blob_stored, NULL, &blob_stored);
tf_ssb_db_blob_store(ssb, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL);
tf_ssb_notify_blob_stored(ssb, blob_id);
tf_ssb_remove_blob_stored_callback(ssb, _blob_stored, &blob_stored);
assert(blob_stored);
JSContext* context = tf_ssb_get_context(ssb);
JSValue obj = JS_NewObject(context);
JS_SetPropertyStr(context, obj, "type", JS_NewString(context, "post"));
JS_SetPropertyStr(context, obj, "text", JS_NewString(context, "First post."));
JSValue mentions = JS_NewArray(context);
JSValue mention = JS_NewObject(context);
JS_SetPropertyStr(context, mention, "link", JS_NewString(context, blob_id));
JS_SetPropertyUint32(context, mentions, 0, mention);
JS_SetPropertyStr(context, obj, "mentions", mentions);
JSValue signed_message = tf_ssb_sign_message(ssb, id, priv, obj, NULL, 0);
char message_id[k_id_base64_len] = { 0 };
tf_ssb_verify_strip_and_store_message(ssb, signed_message, _store_callback, message_id);
JS_FreeValue(context, signed_message);
while (!*message_id)
{
uv_run(tf_ssb_get_loop(ssb), UV_RUN_ONCE);
}
JS_FreeValue(context, obj);
assert(tf_ssb_db_blob_get(ssb, blob_id, NULL, NULL));
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
tf_ssb_db_add_block(db, message_id);
assert(tf_ssb_db_is_blocked(db, message_id));
/* Blocked already, because the blocked message references it. */
assert(tf_ssb_db_is_blocked(db, blob_id));
tf_ssb_db_add_block(db, blob_id);
assert(!tf_ssb_db_blob_get(ssb, blob_id, NULL, NULL));
tf_ssb_db_add_block(db, id);
assert(tf_ssb_db_is_blocked(db, id));
tf_ssb_db_remove_block(db, blob_id);
tf_ssb_db_remove_block(db, message_id);
tf_ssb_db_remove_block(db, id);
assert(!tf_ssb_db_is_blocked(db, message_id));
assert(!tf_ssb_db_is_blocked(db, blob_id));
assert(!tf_ssb_db_is_blocked(db, id));
tf_ssb_release_db_writer(ssb, db);
assert(tf_ssb_db_blob_get(ssb, blob_id, NULL, NULL));
tf_ssb_destroy(ssb);
uv_run(&loop, UV_RUN_DEFAULT);
uv_loop_close(&loop);
}
#endif

View File

@@ -107,4 +107,10 @@ void tf_ssb_test_cli(const tf_test_options_t* options);
*/
void tf_ssb_test_following_perf(const tf_test_options_t* options);
/**
** Test blocks.
** @param options The test options.
*/
void tf_ssb_test_blocks(const tf_test_options_t* options);
/** @} */

View File

@@ -982,7 +982,6 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "b64", _test_b64, false);
_tf_test_run(options, "rooms", tf_ssb_test_rooms, false);
_tf_test_run(options, "bench", tf_ssb_test_bench, false);
_tf_test_run(options, "auto", _test_auto, false);
_tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true);
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
@@ -994,6 +993,8 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "triggers", tf_ssb_test_triggers, false);
_tf_test_run(options, "cli", tf_ssb_test_cli, false);
_tf_test_run(options, "following_perf", tf_ssb_test_following_perf, true);
_tf_test_run(options, "blocks", tf_ssb_test_blocks, true);
_tf_test_run(options, "auto", _test_auto, false);
tf_printf("Tests completed.\n");
#endif
}

View File

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

View File

@@ -179,14 +179,14 @@ try:
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
words = select(driver, ['#document', 'frame', '//li//textarea']).get_attribute('value')
select(driver, ['#document', 'frame', '//li/button[text()="Delete Identity"]'], ('click',))
driver.switch_to.alert.send_keys('DELETE')
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).send_keys('DELETE')
wait.until(expected_conditions.alert_is_present()).accept()
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).accept()
words = select(driver, ['#document', 'frame', '//textarea'], ('send_keys', words))
select(driver, ['#document', 'frame', '//button[text()="Import Identity"]'], ('click',))
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).accept()
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
assert id0 == id1
@@ -197,16 +197,16 @@ try:
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
select(driver, ['//label[text()="Remember this decision."]'], ('click',))
select(driver, ['//button[text()="❌ Deny"]'], ('click',))
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).accept()
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).accept()
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '=🎛️'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#permission_reset:ssb_append'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#permissions_close'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
select(driver, ['//button[text()="❌ Deny"]'], ('click',))
driver.switch_to.alert.accept()
wait.until(expected_conditions.alert_is_present()).accept()
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
select(driver, ['//button[text()="✅ Allow"]'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'Hello, world 2!'))

View File

@@ -18,6 +18,7 @@ k_feeds = {
'manyverse': 'https://gitlab.com/staltz/manyverse/-/commits/master?format=atom',
'ahau': 'https://gitlab.com/ahau/ahau/-/commits/master.atom',
'patchfox': 'https://github.com/soapdog/patchfox/commits.atom',
'ponchowonky': 'https://github.com/soapdog/patchwork/commits.atom',
}
def fix_title(entry):