Compare commits

...

5 Commits

Author SHA1 Message Date
38d746b310 ssb: Dust off the verify command.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m53s
2025-04-09 19:32:38 -04:00
f7270987ea http: Trying to track down a rare 404 I get on save. 2025-04-09 19:08:28 -04:00
6f565c0f0a ssb: Move reply and react to the message % menu. 2025-04-09 19:02:02 -04:00
7f252e79b6 ssb: Faster channel loads. 2025-04-09 18:50:14 -04:00
ba2bb17638 ssb: Gather all the votes for messages seen. 2025-04-09 18:29:37 -04:00
7 changed files with 149 additions and 55 deletions

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&VvfYCIWHlYDtlgOBhDByg1l9rt8NIRt/TCurKx2/AWE=.sha256"
"previous": "&YZCzXrfB6j+y0sXF4KspAibwjLsSCaMoB5rdO3mQl+Q=.sha256"
}

View File

@ -431,6 +431,14 @@ class TfMessageElement extends LitElement {
>
Copy ID
</button>
${this.drafts[this.message?.id] === undefined ? html`
<button class="w3-button w3-bar-item" @click=${this.show_reply}>
Reply
</button>
` : undefined}
<button class="w3-button w3-bar-item w3-border-bottom" @click=${this.react}>
👍 React
</button>
${formats.map(
([format, name]) => html`
<button
@ -530,17 +538,10 @@ class TfMessageElement extends LitElement {
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
: undefined;
return html`
<div class="w3-section w3-container">
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</div>
`;

View File

@ -46,6 +46,53 @@ class TfTabNewsFeedElement extends LitElement {
: this.hash.substring(1);
}
async _fetch_related_messages(messages) {
let refs = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(messages.map((x) => x.id))]
);
let related_messages = await tfrpc.rpc.query(
`
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 messages
JOIN json_each(?2) refs ON messages.id = refs.value
JOIN json_each(?1) AS following ON messages.author = following.value
`,
[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
);
let combined = [].concat(messages, related_messages);
let refs2 = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(combined.map((x) => x.id))]
);
return [].concat(combined, await tfrpc.rpc.query(
`
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 messages
JOIN json_each(?2) refs ON messages.id = refs.value
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE messages.content ->> 'type' = 'vote'
`,
[JSON.stringify(this.following), JSON.stringify(refs2.map((x) => x.ref))]
));
}
async fetch_messages(start_time, end_time) {
this.time_loading = [start_time, end_time];
let result;
@ -107,7 +154,8 @@ class TfTabNewsFeedElement extends LitElement {
[this.hash.substring(1)]
);
} else if (this.hash.startsWith('##')) {
result = await tfrpc.rpc.query(
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
all_news AS (
@ -120,20 +168,11 @@ class TfTabNewsFeedElement extends LitElement {
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4),
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
),
news AS (SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20)
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 news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
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 news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT TRUE AS is_primary, news.* FROM news
`,
[
@ -144,6 +183,12 @@ class TfTabNewsFeedElement extends LitElement {
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
@ -160,7 +205,7 @@ class TfTabNewsFeedElement extends LitElement {
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else {
let t0 = new Date();
result = await tfrpc.rpc.query(
let initial_messages = await tfrpc.rpc.query(
`
WITH
all_news AS (
@ -177,34 +222,11 @@ class TfTabNewsFeedElement extends LitElement {
`,
[JSON.stringify(this.following), start_time, end_time]
);
let news_length = result.length;
let t1 = new Date();
let refs = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(result.map((x) => x.id))]
);
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
let related_messages = await tfrpc.rpc.query(
`
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 messages
JOIN json_each(?2) refs ON messages.id = refs.value
JOIN json_each(?1) AS following ON messages.author = following.value
`,
[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
);
result = [].concat(result, related_messages);
let t3 = new Date();
console.log(
`load of ${result.length} rows took ${(t3 - t0) / 1000} (${(t1 - t0) / 1000} to find ${news_length} messages, ${(t2 - t1) / 1000} to find ${refs.length} refs, and ${(t3 - t2) / 1000} to find ${related_messages.length} related messages) following=${this.following.length} st=${start_time} et=${end_time}`
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
}
this.time_loading = undefined;

View File

@ -1324,6 +1324,11 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
snprintf(save->blob_id, sizeof(save->blob_id), "%s", blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store or property set failed.\n");
save->response = 500;
}
JS_FreeCString(context, new_app_str);
JS_FreeValue(context, new_app_json);
@ -1340,7 +1345,7 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
}
else
{
save->response = 401;
save->response = 403;
}
tf_free(user_app);
}
@ -1352,12 +1357,21 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
snprintf(save->blob_id, sizeof(save->blob_id), "%s", blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store failed.\n");
save->response = 500;
}
}
else
{
save->response = 400;
}
}
else
{
save->response = 401;
}
tf_free((void*)session);
JS_FreeCString(context, user_string);

View File

@ -1204,9 +1204,38 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
return EXIT_FAILURE;
}
tf_printf("Verifying %s...\n", identity);
bool verified = false;
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
bool verified = tf_ssb_db_verify(ssb, identity);
if (identity)
{
tf_printf("Verifying %s...\n", identity);
verified = tf_ssb_db_verify(ssb, identity, true);
}
else
{
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT DISTINCT author FROM messages ORDER BY author", -1, &statement, NULL) == SQLITE_OK)
{
verified = true;
while (sqlite3_step(statement) == SQLITE_ROW)
{
const char* identity = (const char*)sqlite3_column_text(statement, 0);
tf_printf("Verifying %s...", identity);
if (tf_ssb_db_verify(ssb, identity, true))
{
tf_printf("success.\n");
}
else
{
tf_printf("fail.\n");
verified = false;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return verified ? EXIT_SUCCESS : EXIT_FAILURE;

View File

@ -182,13 +182,15 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_size_by_author_index ON messages (author, length(content))");
_tf_ssb_db_exec(
db, "CREATE INDEX IF NOT EXISTS messages_type_author_channel_root_rowid_index ON messages (author, content ->> 'type', content ->> 'channel', content ->> 'root')");
_tf_ssb_db_exec(db,
"CREATE INDEX IF NOT EXISTS messages_type_author_channel_root_timestamp_index ON messages (author, timestamp, content ->> 'type', content ->> 'channel', content ->> "
"'root')");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_type_author_channel_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_author_id_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_by_author_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_timestamp_author_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_id_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_type_author_channel_root_rowid_index");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs ("
" id TEXT PRIMARY KEY,"
@ -2127,7 +2129,25 @@ void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callb
tf_ssb_run_work(ssb, _tf_ssb_db_resolve_index_work, _tf_ssb_db_resolve_index_after_work, request);
}
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
static void _tf_ssb_db_set_flags(tf_ssb_t* ssb, const char* message_id, int flags)
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "UPDATE messages SET flags = ? WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_int(statement, 1, flags) == SQLITE_OK && sqlite3_bind_text(statement, 2, message_id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) != SQLITE_DONE)
{
tf_printf("Setting flags of %s to %d failed: %s.\n", message_id, flags, sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, bool fix)
{
JSContext* context = tf_ssb_get_context(ssb);
bool verified = true;
@ -2158,7 +2178,14 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
if (calculated_flags != flags)
{
tf_printf("author=%s sequence=%" PRId64 " flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
verified = false;
if (fix)
{
_tf_ssb_db_set_flags(ssb, message_id, calculated_flags);
}
else
{
verified = false;
}
}
if (strcmp(message_id, calculated_id))
{

View File

@ -444,9 +444,10 @@ void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callb
** Verify an author's feed.
** @param ssb The SSB instance.
** @param id The author'd identity.
** @param fix Fix invalid messages when possible.
** @return true If the feed verified successfully.
*/
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, bool fix);
/**
** Check if a user has a specific permission.