Compare commits

..

54 Commits

Author SHA1 Message Date
6609a5f340 core: Length of undefined is 0. It's fine. Quiet some errors.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m51s
2024-12-18 20:54:13 -05:00
d9972cb349 tests: Work around an intermittent -t=auto failure. The 'Edit Profile' click is getting lost as things rapidly update? I haven't ever seen it as a human clicking.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m8s
2024-12-18 20:09:50 -05:00
28d2539432 ssb: A first pass at showing private messages next to channels. #84
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-18 20:03:53 -05:00
f28386b71f ssb: Off by one on the unread line.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m32s
2024-12-18 12:46:02 -05:00
53717076f5 ssb: Fix some unread marker issues.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-18 12:43:25 -05:00
a9aa928629 tests: Prefer tf_printf.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m33s
2024-12-17 20:41:27 -05:00
8df121148d update: c-ares 1.34.4.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m29s
2024-12-15 08:33:38 -05:00
5e23c32ae8 build: Fix a potential null dereference?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m9s
2024-12-15 07:53:24 -05:00
9c0f6481c0 ssb: Try to go easier on the main thread, still.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 14m8s
2024-12-14 21:36:33 -05:00
68ae45dd58 ssb: Prevent -t=bench from stalling.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m50s
2024-12-11 20:53:25 -05:00
3091747438 ssb: prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 20:35:32 -05:00
2f266b8dd4 ssb: Attempt to request more feeds as more contact messages come in. #83
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 20:26:28 -05:00
ee20b87ee2 ssb: Alt+up/down to cycle through channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m53s
2024-12-11 12:53:04 -05:00
83e025d0bb update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 12:41:42 -05:00
5115c6e217 ssb: Fix an instance of channels being stuck unread.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m22s
2024-12-10 21:09:55 -05:00
76f6a94de5 ssb: Fix replication hops usage. Thanks @Cashew.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m15s
2024-12-10 19:18:01 -05:00
954830be18 ssb: Allow encrypting/decrypting with the server identity as an admin.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m45s
2024-12-10 12:43:07 -05:00
ea70299a45 update: sqlite 3.47.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m59s
2024-12-08 16:47:21 -05:00
88da071ed6 ssb: We can load more messages by author, now.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m44s
2024-12-08 09:40:02 -05:00
1dbf162a71 ssb: Bring back the updating date while loading.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m54s
2024-12-07 14:58:01 -05:00
1c0964753b ssb: Correctness around loading messages by time range.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m39s
2024-12-07 14:25:19 -05:00
daa1c7f577 ssb: Don't miss contact messages that aren't followed by non-follow messages. 2024-12-07 14:08:53 -05:00
854416ceb2 ssb: Make the depth arg to ssb.following() match the docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m42s
2024-12-07 11:28:33 -05:00
2230351e3e ssb: Show the load more button for mentions.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-07 10:38:34 -05:00
7da3244da2 ssb: prettier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m29s
2024-12-05 20:47:02 -05:00
bfeb0c2988 update: prettier. 2024-12-05 20:46:23 -05:00
d4e75c1dec ssb: Move mentions into the channels sidebar.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-05 20:45:20 -05:00
405bddcde0 ssb: Make the tab bar stay on top of the content. Weird, the random things that were showing up on top.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m35s
2024-12-04 21:23:17 -05:00
8a27c45ab1 ssb: An experiment in including hashtag mentions in channel content. Also sort the channel list like I thought I already did.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m54s
2024-12-04 20:50:46 -05:00
10b15896b3 ssb: Fix the loading cancel button.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m19s
2024-12-04 20:28:57 -05:00
0e97bbe37c android: Fix some crashes, callstacks, and warnings I'm seeing in the logs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m27s
2024-12-04 20:05:50 -05:00
e0d7e90894 ssb: Add an overlay for the sidebar so that it can be closed by tapping back on the content.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m42s
2024-12-03 19:40:08 -05:00
5d13f6aab6 wiki: Back to latest commonmark built as mjs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m26s
2024-12-02 18:45:23 -05:00
1ebfbbe89e wiki: Go back to the last version that worked.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m58s
2024-12-02 17:57:08 -05:00
91ad43fdfc ssb: A more plausibly correct way to load new messages correctly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m51s
2024-12-01 18:20:57 -05:00
6fe6fc180d ssb: New theme, better load, remove debug prints.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m11s
2024-12-01 16:27:59 -05:00
d84d0bec38 ssb: This index help channel status load faster.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-01 16:26:40 -05:00
7e7b1c6ee1 ssb: Make #hashtags direct to channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m25s
2024-12-01 15:32:35 -05:00
effb354d1b ssb: Working toward a more sensible unread indication and user interface for setting read/unread.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m5s
2024-12-01 12:56:31 -05:00
ba7d1ad35f core: This case is not a good cause to crash.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 28m0s
2024-12-01 09:48:15 -05:00
3ca2b19502 ssb: Canceling loads, more mobile-friendly sidebar, and respond to channel subscription changes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m30s
2024-11-30 17:49:36 -05:00
8e0d91dcf5 security: Setting global settings requires approval.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m8s
2024-11-30 16:58:48 -05:00
cd2c2587ae ssb: Merge in the new very work in progress channels interface.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m1s
2024-11-30 15:05:14 -05:00
53044696ba ssb: Just request blobs for all references from about messages for now. Much faster than narrowing down to the most recent images.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m21s
2024-11-29 10:28:16 -05:00
6d6927213f Revert "ssb: Try harder to replicate profile images, even if they were set before our blob replication cutoff."
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
This reverts commit 7f4e2617ee.
2024-11-29 08:54:54 -05:00
be1b5bce4f test: Simplify my selection helper syntax a bit.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m17s
2024-11-28 19:58:51 -05:00
4b4fd0735b test: Make -t auto a bit more resilient by hoisting all the retries into one place that makes sense to me.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 17:51:22 -05:00
c565b2a31f bot: Make sure release messages get through.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 11:11:25 -05:00
55f2261905 prettier: Update the copy of prettier used in the editor. 2024-11-28 11:00:59 -05:00
51912f2b83 ssb: Update emojis.json, and add a script to regenerate it.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 09:16:07 -05:00
7f4e2617ee ssb: Try harder to replicate profile images, even if they were set before our blob replication cutoff.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-27 21:42:54 -05:00
960a385202 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m58s
2024-11-27 15:20:32 -05:00
21f48d3485 build: Let's start work on 0.0.26.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m23s
2024-11-27 12:24:42 -05:00
7f9605e55f nix: Update for 0.0.25. 2024-11-27 12:23:52 -05:00
49 changed files with 1616 additions and 826 deletions

View File

@ -16,11 +16,11 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker. ## LD := Linker.
## ANDROID_SDK := Path to the Android SDK. ## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 30 VERSION_CODE := 31
VERSION_NUMBER := 0.0.25 VERSION_NUMBER := 0.0.26-wip
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470100.zip SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&ksxKqT3Bkp0Z2zV2dQU4ttVZ1k16zdWoJVv6R7m5yAQ=.sha256" "previous": "&0gBRfD+3EaZD2S82zAnXT3hgGuNTnUncOh5vGwZwbSw=.sha256"
} }

View File

@ -7,7 +7,7 @@ function textNode(text) {
function linkNode(text, link) { function linkNode(text, link) {
const linkNode = new commonmark.Node('link', undefined); const linkNode = new commonmark.Node('link', undefined);
if (link.startsWith('#')) { if (link.startsWith('#')) {
linkNode.destination = `#q=${encodeURIComponent(link)}`; linkNode.destination = `#${encodeURIComponent('#' + link)}`;
} else { } else {
linkNode.destination = link; linkNode.destination = link;
} }

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,6 @@ import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js'; import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.js'; import * as tf_profile from './tf-profile.js';
import * as tf_reactions_modal from './tf-reactions-modal.js'; import * as tf_reactions_modal from './tf-reactions-modal.js';
import * as tf_tab_mentions from './tf-tab-mentions.js';
import * as tf_tab_news from './tf-tab-news.js'; import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js'; import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js'; import * as tf_tab_search from './tf-tab-search.js';

View File

@ -16,7 +16,9 @@ class TfElement extends LitElement {
following: {type: Array}, following: {type: Array},
users: {type: Object}, users: {type: Object},
ids: {type: Array}, ids: {type: Array},
tags: {type: Array}, channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
}; };
} }
@ -33,7 +35,9 @@ class TfElement extends LitElement {
this.following = []; this.following = [];
this.users = {}; this.users = {};
this.loaded = false; this.loaded = false;
this.tags = []; this.channels = [];
this.channels_unread = {};
this.channels_latest = {};
tfrpc.rpc.getBroadcasts().then((b) => { tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || []; self.broadcasts = b || [];
}); });
@ -64,6 +68,68 @@ class TfElement extends LitElement {
let ids = (await tfrpc.rpc.getIdentities()) || []; let ids = (await tfrpc.rpc.getIdentities()) || [];
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.ids = ids; this.ids = ids;
await this.load_channels();
}
async load_channels() {
let channels = await tfrpc.rpc.query(
`
SELECT
content ->> 'channel' AS channel,
content ->> 'subscribed' AS subscribed
FROM
messages
WHERE
author = ? AND
content ->> 'type' = 'channel'
ORDER BY sequence
`,
[this.whoami]
);
let channel_map = {};
for (let row of channels) {
if (row.subscribed) {
channel_map[row.channel] = true;
} else {
delete channel_map[row.channel];
}
}
this.channels = Object.keys(channel_map).sort();
}
connectedCallback() {
super.connectedCallback();
this._keydown = this.keydown.bind(this);
window.addEventListener('keydown', this._keydown);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this._keydown);
}
keydown(event) {
if (event.altKey && event.key == 'ArrowUp') {
this.next_channel(1);
event.preventDefault();
} else if (event.altKey && event.key == 'ArrowDown') {
this.next_channel(-1);
event.preventDefault();
}
}
next_channel(delta) {
let channel_names = ['', '@'].concat(this.channels);
let index = channel_names.indexOf(this.hash.substring(1));
if (index != -1) {
index += delta;
this.set_hash(
'#' +
encodeURIComponent(
channel_names[(index + channel_names.length) % channel_names.length]
)
);
}
} }
set_hash(hash) { set_hash(hash) {
@ -72,8 +138,6 @@ class TfElement extends LitElement {
this.tab = 'search'; this.tab = 'search';
} else if (this.hash === '#connections') { } else if (this.hash === '#connections') {
this.tab = 'connections'; this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) { } else if (this.hash.startsWith('#sql=')) {
this.tab = 'query'; this.tab = 'query';
} else { } else {
@ -167,6 +231,14 @@ class TfElement extends LitElement {
`, `,
[JSON.stringify(this.following), id] [JSON.stringify(this.following), id]
); );
for (let message of messages) {
if (message.author == this.whoami) {
let content = JSON.parse(message.content);
if (content?.type == 'channel') {
this.load_channels();
}
}
}
if (messages && messages.length) { if (messages && messages.length) {
this.unread = [...this.unread, ...messages]; this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024); this.unread = this.unread.slice(this.unread.length - 1024);
@ -195,33 +267,37 @@ class TfElement extends LitElement {
} }
} }
async load_recent_tags() { async get_latest_private(following) {
let start = new Date(); let latest = (await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages'))[0].latest;
this.tags = await tfrpc.rpc.query( const k_chunk_count = 256;
` while (latest - k_chunk_count >= 0) {
WITH let messages = await tfrpc.rpc.query(`
recent AS (SELECT id, json(content) AS content FROM messages SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post' FROM messages
ORDER BY timestamp DESC LIMIT 1024), JOIN json_each(?1) AS following ON messages.author = following.value
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag WHERE
FROM recent messages.rowid > ?2 AND
WHERE json_extract(content, '$.channel') IS NOT NULL), messages.rowid <= ?3 AND
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag json(messages.content) LIKE '"%'
FROM recent, json_each(recent.content, '$.mentions') AS mention ORDER BY sequence DESC
WHERE json_valid(mention.value) AND tag LIKE '#%'),
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`, `,
[new Date() - 7 * 24 * 60 * 60 * 1000] [
); JSON.stringify(following),
console.log('tags took', (new Date() - start) / 1000.0, 'seconds'); latest - k_chunk_count,
latest,
]);
messages = (await this.decrypt(messages)).filter(x => x.decrypted);
if (messages.length) {
return Math.max(...messages.map(x => x.rowid));
}
latest -= k_chunk_count;
};
return -1;
} }
async load() { async load() {
let whoami = this.whoami; let whoami = this.whoami;
let tags = this.load_recent_tags(); let following = await tfrpc.rpc.following([whoami], 2);
let following = await tfrpc.rpc.following([whoami], 3);
let users = {}; let users = {};
let by_count = []; let by_count = [];
for (let [id, v] of Object.entries(following)) { for (let [id, v] of Object.entries(following)) {
@ -233,7 +309,31 @@ class TfElement extends LitElement {
}; };
by_count.push({count: v.of, id: id}); by_count.push({count: v.of, id: id});
} }
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20)); let channels = tfrpc.rpc.query(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.content ->> 'type' = 'post' AND messages.content ->> 'root' IS NULL
GROUP by channel
UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
`,
[
JSON.stringify(this.channels),
JSON.stringify(Object.keys(following)),
'"' + this.whoami.replace('"', '""') + '"',
]
);
let latest_private = this.get_latest_private(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
let start_time = new Date(); let start_time = new Date();
users = await this.fetch_about(Object.keys(following).sort(), users); users = await this.fetch_about(Object.keys(following).sort(), users);
console.log( console.log(
@ -243,14 +343,54 @@ class TfElement extends LitElement {
Object.keys(users).length, Object.keys(users).length,
'users' 'users'
); );
start_time = new Date();
channels = await channels;
console.log('channels took', (new Date() - start_time) / 1000.0);
this.channels_latest = Object.fromEntries(
channels.map((x) => [x.channel, x.rowid])
);
let self = this;
latest_private.then(function(latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {'🔐': latest});
console.log('private took', (new Date() - start_time) / 1000.0);
});
this.following = Object.keys(following); this.following = Object.keys(following);
this.users = users; this.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`); console.log(`load finished ${whoami} => ${this.whoami}`);
this.whoami = whoami; this.whoami = whoami;
this.loaded = whoami; this.loaded = whoami;
} }
channel_set_unread(event) {
this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
this.channels_unread = Object.assign({}, this.channels_unread);
tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
}
async decrypt(messages) {
let whoami = this.whoami;
return Promise.all(messages.map(async function (message) {
let content;
try {
content = JSON.parse(message?.content);
} catch {}
if (typeof content === 'string') {
let decrypted;
try {
decrypted = await tfrpc.rpc.try_decrypt(whoami, content);
} catch {}
if (decrypted) {
try {
message.decrypted = JSON.parse(decrypted);
} catch {
message.decrypted = decrypted;
}
}
}
return message;
}));
}
render_tab() { render_tab() {
let following = this.following; let following = this.following;
let users = this.users; let users = this.users;
@ -265,6 +405,10 @@ class TfElement extends LitElement {
.unread=${this.unread} .unread=${this.unread}
@refresh=${() => (this.unread = [])} @refresh=${() => (this.unread = [])}
?loading=${this.loading} ?loading=${this.loading}
.channels=${this.channels}
.channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread}
></tf-tab-news> ></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {
@ -275,14 +419,6 @@ class TfElement extends LitElement {
.broadcasts=${this.broadcasts} .broadcasts=${this.broadcasts}
></tf-tab-connections> ></tf-tab-connections>
`; `;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions
.following=${this.following}
whoami=${this.whoami}
.users="${this.users}}"
></tf-tab-mentions>
`;
} else if (this.tab === 'search') { } else if (this.tab === 'search') {
return html` return html`
<tf-tab-search <tf-tab-search
@ -314,8 +450,6 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#'); await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') { } else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections'); await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') { } else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql='); await tfrpc.rpc.setHash('#sql=');
} }
@ -338,13 +472,15 @@ class TfElement extends LitElement {
const k_tabs = { const k_tabs = {
'📰': 'news', '📰': 'news',
'📡': 'connections', '📡': 'connections',
'@': 'mentions',
'🔍': 'search', '🔍': 'search',
'👩‍💻': 'query', '👩‍💻': 'query',
}; };
let tabs = html` let tabs = html`
<div class="w3-bar w3-theme-l1"> <div
class="w3-bar w3-theme-l1"
style="position: sticky; top: 0; z-index: 10"
>
<button <button
class="w3-bar-item w3-button w3-circle w3-ripple" class="w3-bar-item w3-button w3-circle w3-ripple"
@click=${this.refresh} @click=${this.refresh}
@ -384,13 +520,7 @@ class TfElement extends LitElement {
style="width: 100vw; min-height: 100vh; height: 100%" style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark" class="w3-theme-dark"
> >
${tabs} ${tabs} ${contents}
<div style="padding: 8px">
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
</div>
</div> </div>
`; `;
} }

View File

@ -14,6 +14,7 @@ class TfComposeElement extends LitElement {
apps: {type: Object}, apps: {type: Object},
drafts: {type: Object}, drafts: {type: Object},
author: {type: String}, author: {type: String},
channel: {type: String},
}; };
} }
@ -196,6 +197,7 @@ class TfComposeElement extends LitElement {
let message = { let message = {
type: 'post', type: 'post',
text: edit.innerText, text: edit.innerText,
channel: this.channel,
}; };
if (this.root || this.branch) { if (this.root || this.branch) {
message.root = this.root; message.root = this.root;
@ -535,6 +537,9 @@ class TfComposeElement extends LitElement {
class="w3-card-4 w3-theme-d4 w3-padding-small" class="w3-card-4 w3-theme-d4 w3-padding-small"
style="box-sizing: border-box" style="box-sizing: border-box"
> >
${this.channel !== undefined
? html`<p>To #${this.channel}:</p>`
: undefined}
${this.render_encrypt()} ${this.render_encrypt()}
<div class="w3-container w3-padding-small"> <div class="w3-container w3-padding-small">
<div class="w3-half"> <div class="w3-half">

View File

@ -14,6 +14,8 @@ class TfMessageElement extends LitElement {
format: {type: String}, format: {type: String},
blog_data: {type: String}, blog_data: {type: String},
expanded: {type: Object}, expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
}; };
} }
@ -28,6 +30,7 @@ class TfMessageElement extends LitElement {
this.drafts = {}; this.drafts = {};
this.format = 'message'; this.format = 'message';
this.expanded = {}; this.expanded = {};
this.channel_unread = -1;
} }
show_reply() { show_reply() {
@ -228,7 +231,7 @@ class TfMessageElement extends LitElement {
>${mention.name}</a >${mention.name}</a
>`; >`;
} else if (mention.link?.startsWith('#')) { } else if (mention.link?.startsWith('#')) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)} return html` <a href=${'#' + encodeURIComponent('#' + mention.link)}
>${mention.link}</a >${mention.link}</a
>`; >`;
} else if ( } else if (
@ -312,12 +315,27 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>` ></tf-message>`
)}`; )}`;
} }
} }
} }
mark_unread() {
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel,
unread: this.message.rowid,
},
})
);
}
render_channels() { render_channels() {
let content = this.message?.content; let content = this.message?.content;
if (this?.messsage?.decrypted?.type == 'post') { if (this?.messsage?.decrypted?.type == 'post') {
@ -344,6 +362,8 @@ ${JSON.stringify(mention, null, 2)}</pre
} }
let class_background = this.message?.decrypted let class_background = this.message?.decrypted
? 'w3-pale-red' ? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4'; : 'w3-theme-d4';
let self = this; let self = this;
let raw_button; let raw_button;
@ -423,6 +443,8 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${self.users} .users=${self.users}
.drafts=${self.drafts} .drafts=${self.drafts}
.expanded=${self.expanded} .expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
></tf-message> ></tf-message>
` `
)} )}
@ -442,6 +464,8 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>` ></tf-message>`
)} )}
</div>`; </div>`;
@ -463,6 +487,8 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message> ></tf-message>
` `
)} )}
@ -618,6 +644,16 @@ ${JSON.stringify(content, null, 2)}</pre
<button class="w3-button w3-theme-d1" @click=${this.react}> <button class="w3-button w3-theme-d1" @click=${this.react}>
React React
</button> </button>
${!content.root && this.message.rowid < this.channel_unread
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.mark_unread}
>
Mark Unread
</button>
`
: undefined}
</p> </p>
${this.render_children()} ${this.render_children()}
</div> </div>
@ -787,7 +823,7 @@ ${JSON.stringify(content, null, 2)}</pre
return small_frame(html` return small_frame(html`
<div> <div>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} ${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#q=' + encodeURIComponent('#' + content.channel)} <a href=${'#' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a >#${content.channel}</a
> >
</div> </div>

View File

@ -11,6 +11,8 @@ class TfNewsElement extends LitElement {
following: {type: Array}, following: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
}; };
} }
@ -25,6 +27,7 @@ class TfNewsElement extends LitElement {
this.following = []; this.following = [];
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channel_unread = -1;
} }
process_messages(messages) { process_messages(messages) {
@ -33,12 +36,13 @@ class TfNewsElement extends LitElement {
console.log('processing', messages.length, 'messages'); console.log('processing', messages.length, 'messages');
function ensure_message(id) { function ensure_message(id, rowid) {
let found = messages_by_id[id]; let found = messages_by_id[id];
if (found) { if (found) {
return found; return found;
} else { } else {
let added = { let added = {
rowid: rowid,
id: id, id: id,
placeholder: true, placeholder: true,
content: '"placeholder"', content: '"placeholder"',
@ -53,7 +57,7 @@ class TfNewsElement extends LitElement {
function link_message(message) { function link_message(message) {
if (message.content.type === 'vote') { if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link); let parent = ensure_message(message.content.vote.link, message.rowid);
if (!parent.votes) { if (!parent.votes) {
parent.votes = []; parent.votes = [];
} }
@ -62,14 +66,14 @@ class TfNewsElement extends LitElement {
} else if (message.content.type == 'post') { } else if (message.content.type == 'post') {
if (message.content.root) { if (message.content.root) {
if (typeof message.content.root === 'string') { if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root); let m = ensure_message(message.content.root, message.rowid);
if (!m.child_messages) { if (!m.child_messages) {
m.child_messages = []; m.child_messages = [];
} }
m.child_messages.push(message); m.child_messages.push(message);
message.parent_message = message.content.root; message.parent_message = message.content.root;
} else { } else {
let m = ensure_message(message.content.root[0]); let m = ensure_message(message.content.root[0], message.rowid);
if (!m.child_messages) { if (!m.child_messages) {
m.child_messages = []; m.child_messages = [];
} }
@ -162,6 +166,7 @@ class TfNewsElement extends LitElement {
} else { } else {
if (group.length > 0) { if (group.length > 0) {
result.push({ result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group', type: 'contact_group',
messages: group, messages: group,
}); });
@ -170,6 +175,13 @@ class TfNewsElement extends LitElement {
result.push(message); result.push(message);
} }
} }
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
}
return result; return result;
} }
@ -178,18 +190,39 @@ class TfNewsElement extends LitElement {
let final_messages = this.group_following( let final_messages = this.group_following(
this.finalize_messages(messages_by_id) this.finalize_messages(messages_by_id)
); );
let unread_rowid = -1;
for (let message of final_messages) {
if (message.rowid >= this.channel_unread) {
unread_rowid = message.rowid;
}
}
return html` return html`
<div style="display: flex; flex-direction: column"> <div>
${final_messages.map( ${final_messages.map(
(x) => (x) =>
html`<tf-message html`
<tf-message
.message=${x} .message=${x}
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
collapsed="true" collapsed="true"
></tf-message>` channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
${x.rowid == unread_rowid && x != final_messages[0]
? html`<div style="display: flex; flex-direction: row">
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
<div style="color: #f00; padding: 8px">unread</div>
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
</div>`
: undefined}
`
)} )}
</div> </div>
`; `;

View File

@ -286,29 +286,29 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
`; `;
// prettier-ignore // prettier-ignore
const w3_2016_riverside = css` const w3_2016_snorkel_blue = css`
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important} .w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important} .w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important} .w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important} .w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important} .w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important} .w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important} .w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important} .w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important} .w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important} .w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important} .w3-theme-light {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-dark {color:#fff !important; background-color:#26364a !important} .w3-theme-dark {color:#fff !important; background-color:#012641 !important}
.w3-theme-action {color:#fff !important; background-color:#26364a !important} .w3-theme-action {color:#fff !important; background-color:#012641 !important}
.w3-theme {color:#fff !important; background-color:#4c6a92 !important} .w3-theme {color:#fff !important; background-color:#034f84 !important}
.w3-text-theme {color:#4c6a92 !important} .w3-text-theme {color:#034f84 !important}
.w3-border-theme {border-color:#4c6a92 !important} .w3-border-theme {border-color:#034f84 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important} .w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important}
.w3-hover-text-theme:hover {color:#4c6a92 !important} .w3-hover-text-theme:hover {color:#034f84 !important}
.w3-hover-border-theme:hover {border-color:#4c6a92 !important} .w3-hover-border-theme:hover {border-color:#034f84 !important}
`; `;
export let styles = [tf, w3, w3_2016_riverside]; export let styles = [tf, w3, w3_2016_snorkel_blue];

View File

@ -1,78 +0,0 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabMentionsElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
following: {type: Array},
expanded: {type: Object},
messages: {type: Array},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
this.messages = [];
}
async load() {
console.log('Loading...', this.whoami);
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
WHERE messages.author != ?
ORDER BY timestamp DESC limit 20
`,
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
this.whoami,
]
);
console.log('Done.');
this.messages = results;
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render() {
let self = this;
if (!this.loading) {
this.loading = true;
this.load();
}
return html`
<tf-news
id="news"
whoami=${this.whoami}
.messages=${this.messages}
.users=${this.users}
.expanded=${this.expanded}
@tf-expand=${this.on_expand}
></tf-news>
`;
}
}
customElements.define('tf-tab-mentions', TfTabMentionsElement);

View File

@ -12,6 +12,11 @@ class TfTabNewsFeedElement extends LitElement {
messages: {type: Array}, messages: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
channels_unread: {type: Object},
channels_latest: {type: Object},
loading: {type: Number},
time_range: {type: Array},
time_loading: {type: Array},
}; };
} }
@ -26,30 +31,67 @@ class TfTabNewsFeedElement extends LitElement {
this.following = []; this.following = [];
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000; this.channels_unread = {};
this.channels_latest = {};
this.start_time = new Date().valueOf();
this.time_range = [0, 0];
this.time_loading = undefined;
this.loading = 0;
} }
async fetch_messages() { channel() {
if (this.hash.startsWith('#@')) { return this.hash.startsWith('##')
let r = await tfrpc.rpc.query( ? this.hash.substring(2)
: this.hash.substring(1);
}
async fetch_messages(start_time, end_time) {
this.time_loading = [start_time, end_time];
let result;
if (this.hash == '#@') {
result = await tfrpc.rpc.query(
` `
WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?1)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.author != ?1 AND
messages.timestamp >= ?3 AND
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20
`,
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
start_time,
end_time,
]
);
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE messages.author = ? WHERE messages.author = ?
ORDER BY sequence DESC ORDER BY sequence DESC)
LIMIT 20) SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mine FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
UNION UNION
SELECT * FROM mine SELECT * FROM mine
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
`, `,
[this.hash.substring(1)] [this.hash.substring(1), start_time, end_time]
); );
return r;
} else if (this.hash.startsWith('#%')) { } else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
@ -62,6 +104,68 @@ class TfTabNewsFeedElement extends LitElement {
`, `,
[this.hash.substring(1)] [this.hash.substring(1)]
); );
} else if (this.hash.startsWith('##')) {
let promises = [];
const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(
tfrpc.rpc.query(
`
WITH news 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
JOIN json_each(?) AS following ON messages.author = following.value
WHERE
messages.timestamp >= ? AND
messages.timestamp < ? AND
messages.content ->> 'channel' = ?
ORDER BY messages.timestamp DESC)
SELECT 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 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 messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
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
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
)
);
}
result = [].concat(...(await Promise.all(promises)));
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[JSON.stringify(this.following), start_time, end_time]
);
result = (await this.decrypt(result)).filter(x => x.decrypted);
} else { } else {
let promises = []; let promises = [];
const k_following_limit = 256; const k_following_limit = 256;
@ -69,17 +173,17 @@ class TfTabNewsFeedElement extends LitElement {
promises.push( promises.push(
tfrpc.rpc.query( tfrpc.rpc.query(
` `
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature WITH news 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 FROM messages
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? AND messages.timestamp < ? WHERE messages.timestamp >= ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC) ORDER BY messages.timestamp DESC)
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.ref JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.message JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id JOIN messages ON messages_refs.ref = messages.id
@ -88,50 +192,58 @@ class TfTabNewsFeedElement extends LitElement {
`, `,
[ [
JSON.stringify(this.following.slice(i, i + k_following_limit)), JSON.stringify(this.following.slice(i, i + k_following_limit)),
this.start_time, start_time,
/* end_time,
** Don't show messages more than a day into the future to prevent
** messages with far-future timestamps from staying at the top forever.
*/
new Date().valueOf() + 24 * 60 * 60 * 1000,
] ]
) )
); );
} }
return [].concat(...(await Promise.all(promises))); result = [].concat(...(await Promise.all(promises)));
} }
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
this.time_range = [
messages.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
messages.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1]
),
];
} }
async load_more() { async load_more() {
this.loading++;
this.loading_canceled = false;
try {
let more = [];
while (!more.length && !this.loading_canceled) {
let last_start_time = this.start_time; let last_start_time = this.start_time;
this.start_time = last_start_time - 24 * 60 * 60 * 1000; this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
let more = await tfrpc.rpc.query( more = await this.fetch_messages(this.start_time, last_start_time);
` this.update_time_range_from_messages(
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature more.filter(
FROM messages (x) =>
JOIN json_each(?) AS following ON messages.author = following.value x.timestamp >= this.start_time && x.timestamp < last_start_time
WHERE messages.timestamp > ? )
AND messages.timestamp <= ?
ORDER BY messages.timestamp DESC)
SELECT 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 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 news.* FROM news
`,
[JSON.stringify(this.following), this.start_time, last_start_time]
); );
}
this.messages = await this.decrypt([...more, ...this.messages]); this.messages = await this.decrypt([...more, ...this.messages]);
} finally {
this.loading--;
}
}
cancel_load() {
this.loading_canceled = true;
} }
async decrypt(messages) { async decrypt(messages) {
console.log('decrypt');
let result = []; let result = [];
for (let message of messages) { for (let message of messages) {
let content; let content;
@ -156,8 +268,89 @@ class TfTabNewsFeedElement extends LitElement {
return result; return result;
} }
async add_messages(messages) { async load_latest() {
this.messages = await this.decrypt([...messages, ...this.messages]); this.loading++;
let now = new Date().valueOf();
let end_time = now + 24 * 60 * 60 * 1000;
let messages = [];
try {
messages = await this.fetch_messages(this.time_range[1], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[1] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = [...this.messages, ...messages];
console.log('done loading latest messages.');
}
async load_messages() {
let self = this;
this.loading++;
let messages = [];
try {
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(
this.time_range[0],
this.time_range[1]
);
this.update_time_range_from_messages(
messages.filter(
(x) =>
x.timestamp >= this.time_range[0] &&
x.timestamp < this.time_range[1]
)
);
messages = await this.decrypt(messages);
if (!messages.length) {
let more = [];
while (!more.length && start_time >= 0) {
let last_start_time = start_time;
start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) => x.timestamp >= start_time && x.timestamp < last_start_time
)
);
}
messages = await this.decrypt([...more, ...this.messages]);
}
} finally {
this.loading--;
}
this.messages = messages;
this.time_loading = undefined;
console.log(`loading messages done for ${self.whoami}`);
}
mark_all_read() {
let newest = this.messages.reduce(
(accumulator, current) => Math.max(accumulator, current.rowid),
this.channels_latest[this.channel()] ?? -1
);
if (newest >= 0) {
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel(),
unread: newest + 1,
},
})
);
}
} }
render() { render() {
@ -169,31 +362,49 @@ class TfTabNewsFeedElement extends LitElement {
console.log( console.log(
`loading messages for ${this.whoami} (following ${this.following.length})` `loading messages for ${this.whoami} (following ${this.following.length})`
); );
let self = this; this.load_messages();
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages()
.then(this.decrypt.bind(this))
.then(function (messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
})
.catch(function (error) {
alert(JSON.stringify(error, null, 2));
});
} }
let more; let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) { if (!this.hash.startsWith('#%')) {
more = html` more = html`
<p> <p>
<button class="w3-button w3-theme-d1" @click=${this.load_more}> <button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<button
?disabled=${this.loading}
class="w3-button w3-theme-d1"
@click=${this.load_more}
>
Load More Load More
</button> </button>
<button
class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
@click=${this.cancel_load}
>
Cancel
</button>
<span
>Showing
${new Date(
this.time_loading
? Math.min(this.time_loading[0], this.time_range[0])
: this.time_range[0]
).toLocaleDateString()}
-
${new Date(
this.time_loading
? Math.max(this.time_loading[1], this.time_range[1])
: this.time_range[1]
).toLocaleDateString()}.</span
>
</p> </p>
`; `;
} }
return html` return html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<tf-news <tf-news
id="news" id="news"
whoami=${this.whoami} whoami=${this.whoami}
@ -202,6 +413,8 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following} .following=${this.following}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news> ></tf-news>
${more} ${more}
`; `;

View File

@ -13,6 +13,9 @@ class TfTabNewsElement extends LitElement {
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
loading: {type: Boolean}, loading: {type: Boolean},
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
}; };
} }
@ -29,6 +32,9 @@ class TfTabNewsElement extends LitElement {
this.cache = {}; this.cache = {};
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channels_unread = {};
this.channels_latest = {};
this.channels = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) { tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}'); self.drafts = JSON.parse(d || '{}');
}); });
@ -48,10 +54,7 @@ class TfTabNewsElement extends LitElement {
let unread = this.unread; let unread = this.unread;
let news = this.shadowRoot?.getElementById('news'); let news = this.shadowRoot?.getElementById('news');
if (news) { if (news) {
console.log('injecting messages', news.messages); news.load_latest();
news.add_messages(
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
);
this.dispatchEvent(new CustomEvent('refresh')); this.dispatchEvent(new CustomEvent('refresh'));
} }
} }
@ -106,8 +109,50 @@ class TfTabNewsElement extends LitElement {
} }
} }
unread_status(channel) {
if (
this.channels_latest[channel] &&
(this.channels_unread[channel] === undefined ||
this.channels_unread[channel] < this.channels_latest[channel])
) {
return '🔵';
}
}
show_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'block';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
}
hide_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'none';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
}
async channel_toggle_subscribed() {
let channel = this.hash.substring(2);
let subscribed = this.channels.indexOf(channel) != -1;
subscribed = !subscribed;
await tfrpc.rpc.appendMessage(this.whoami, {
type: 'channel',
channel: channel,
subscribed: subscribed,
});
if (subscribed) {
this.channels = [].concat([channel], this.channels).sort();
} else {
this.channels = this.channels.filter((x) => x != channel);
}
}
channel() {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
}
render() { render() {
let profile = this.hash.startsWith('#@') let profile =
this.hash.startsWith('#@') && this.hash != '#@'
? html`<tf-profile ? html`<tf-profile
class="tf-profile" class="tf-profile"
id=${this.hash.substring(1)} id=${this.hash.substring(1)}
@ -129,13 +174,88 @@ class TfTabNewsElement extends LitElement {
</div>`; </div>`;
} }
return html` return html`
<p class="w3-bar"> <div
<button class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
class="w3-bar-item w3-button w3-theme-d1" style="width: 2in; left: 0; z-index: 5"
@click=${this.show_more} id="sidebar"
> >
<div
class="w3-right w3-button w3-hide-large"
@click=${this.hide_sidebar}
>
&times;
</div>
${this.hash.startsWith('##') &&
this.channels.indexOf(this.hash.substring(2)) == -1
? html`
<div class="w3-bar-item w3-theme-d2">Viewing</div>
<a
href="#"
class="w3-bar-item w3-button"
style="font-weight: bold"
>${this.hash.substring(2)}</a
>
`
: undefined}
<div class="w3-bar-item w3-theme-d2">Channels</div>
<a
href="#"
class="w3-bar-item w3-button"
style=${this.hash == '#' ? 'font-weight: bold' : undefined}
>general ${this.unread_status('')}</a
>
<a
href="#@"
class="w3-bar-item w3-button"
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>@mentions ${this.unread_status('@')}</a
>
<a
href="#🔐"
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>🔐private ${this.unread_status('🔐')}</a
>
${this.channels.map(
(x) => html`
<a
href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
>#${x} ${this.unread_status(x)}</a
>
`
)}
</div>
<div
class="w3-overlay"
id="sidebar_overlay"
@click=${this.hide_sidebar}
></div>
<div style="margin-left: 2in; padding: 8px" id="main" class="w3-main">
<div
id="show_sidebar"
class="w3-left w3-button w3-hide-large"
@click=${this.show_sidebar}
>
&#9776;
</div>
<p>
<button class="w3-button w3-theme-d1" @click=${this.show_more}>
${this.new_messages_text()} ${this.new_messages_text()}
</button> </button>
${this.hash.startsWith('##')
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.channel_toggle_subscribed}
>
${this.channels.indexOf(this.hash.substring(2)) != -1
? 'Unsubscribe from #'
: 'Subscribe to #'}${this.hash.substring(2)}
</button>
`
: undefined}
</p> </p>
<div class="w3-bar"> <div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>! Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
@ -148,6 +268,7 @@ class TfTabNewsElement extends LitElement {
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-draft=${this.draft} @tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose> ></tf-compose>
</div> </div>
${profile} ${profile}
@ -161,7 +282,10 @@ class TfTabNewsElement extends LitElement {
.expanded=${this.expanded} .expanded=${this.expanded}
@tf-draft=${this.draft} @tf-draft=${this.draft}
@tf-expand=${this.on_expand} @tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed> ></tf-tab-news-feed>
</div>
`; `;
} }
} }

View File

@ -18,7 +18,7 @@ class TfTagElement extends LitElement {
render() { render() {
let number = this.count ? html` (${this.count})` : undefined; let number = this.count ? html` (${this.count})` : undefined;
return html`<a return html`<a
href="#q=${this.tag}" href=${'#' + encodeURIComponent(this.tag)}
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
>${this.tag}${number}</a >${this.tag}${number}</a
>`; >`;

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "📝", "emoji": "📝",
"previous": "&4F4D8+QlJVaxXywChQrNTdSV4Y3TvJ0xxqdq/i9HUWA=.sha256" "previous": "&4UHlsfQJvSh7L3D86uFtr7KUKCMRVBBTFxRIMqIc5as=.sha256"
} }

File diff suppressed because one or more lines are too long

View File

@ -419,6 +419,7 @@ async function getProcessBlob(blobId, key, options) {
return settings?.[key]; return settings?.[key];
}; };
imports.core.globalSettingsSet = async function (key, value) { imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest('set_global_setting');
print('Setting', key, value); print('Setting', key, value);
let settings = await loadSettings(); let settings = await loadSettings();
settings[key] = value; settings[key] = value;

View File

@ -21,14 +21,14 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.0.24"; version = "0.0.25";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-XlmRr08UmScY//qxUEXHzagXHCFqARRYr3q8RK/jKFY="; hash = "sha256-Rfk+CUhi+Ss0z70CCgmtVM/w4nCL1GX/MsD4sPYIa5s=";
fetchSubmodules = true; fetchSubmodules = true;
}; };

2
deps/c-ares vendored

@ -1 +1 @@
Subproject commit c29e75d54c3743783d51a609980495cf553b4bca Subproject commit b82840329a4081a1f1b125e6e6b760d4e1237b52

File diff suppressed because one or more lines are too long

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

@ -49,9 +49,9 @@
} }
}, },
"node_modules/@codemirror/lang-css": { "node_modules/@codemirror/lang-css": {
"version": "6.3.0", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
@ -104,9 +104,9 @@
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.3", "version": "6.10.6",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.6.tgz",
"integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", "integrity": "sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -118,20 +118,20 @@
} }
}, },
"node_modules/@codemirror/lint": { "node_modules/@codemirror/lint": {
"version": "6.8.2", "version": "6.8.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.35.0",
"crelt": "^1.0.5" "crelt": "^1.0.5"
} }
}, },
"node_modules/@codemirror/search": { "node_modules/@codemirror/search": {
"version": "6.5.7", "version": "6.5.8",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz",
"integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==", "integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -140,10 +140,13 @@
} }
}, },
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
"version": "6.4.1", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"license": "MIT" "license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
}, },
"node_modules/@codemirror/theme-one-dark": { "node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2", "version": "6.1.2",
@ -158,12 +161,12 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.34.3", "version": "6.35.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.35.3.tgz",
"integrity": "sha512-Ph5d+u8DxIeSgssXEakaakImkzBV4+slwIbcxl9oc9evexJhImeu/G8TK7+zp+IFK9KuJ0BdSn6kTBJeH2CHvA==", "integrity": "sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4" "w3c-keyname": "^2.2.4"
} }
@ -270,9 +273,9 @@
} }
}, },
"node_modules/@lezer/javascript": { "node_modules/@lezer/javascript": {
"version": "1.4.19", "version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==", "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
@ -300,6 +303,12 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.0", "version": "15.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
@ -370,9 +379,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
"integrity": "sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==", "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -383,9 +392,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
"integrity": "sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==", "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -396,9 +405,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
"integrity": "sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==", "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -409,9 +418,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
"integrity": "sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==", "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -422,9 +431,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
"integrity": "sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==", "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -435,9 +444,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
"integrity": "sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==", "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -448,9 +457,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
"integrity": "sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==", "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -461,9 +470,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
"integrity": "sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==", "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -474,9 +483,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
"integrity": "sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==", "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -487,9 +496,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
"integrity": "sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==", "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -499,10 +508,23 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
"integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
"integrity": "sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==", "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -513,9 +535,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
"integrity": "sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==", "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -526,9 +548,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
"integrity": "sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==", "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -539,9 +561,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
"integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==", "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -552,9 +574,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
"integrity": "sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==", "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -565,9 +587,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
"integrity": "sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==", "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -578,9 +600,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
"integrity": "sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==", "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -591,9 +613,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
"integrity": "sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==", "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -780,9 +802,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.27.3", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
"integrity": "sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==", "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.6" "@types/estree": "1.0.6"
@ -795,24 +817,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.27.3", "@rollup/rollup-android-arm-eabi": "4.28.1",
"@rollup/rollup-android-arm64": "4.27.3", "@rollup/rollup-android-arm64": "4.28.1",
"@rollup/rollup-darwin-arm64": "4.27.3", "@rollup/rollup-darwin-arm64": "4.28.1",
"@rollup/rollup-darwin-x64": "4.27.3", "@rollup/rollup-darwin-x64": "4.28.1",
"@rollup/rollup-freebsd-arm64": "4.27.3", "@rollup/rollup-freebsd-arm64": "4.28.1",
"@rollup/rollup-freebsd-x64": "4.27.3", "@rollup/rollup-freebsd-x64": "4.28.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.27.3", "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
"@rollup/rollup-linux-arm-musleabihf": "4.27.3", "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
"@rollup/rollup-linux-arm64-gnu": "4.27.3", "@rollup/rollup-linux-arm64-gnu": "4.28.1",
"@rollup/rollup-linux-arm64-musl": "4.27.3", "@rollup/rollup-linux-arm64-musl": "4.28.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.27.3", "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
"@rollup/rollup-linux-riscv64-gnu": "4.27.3", "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
"@rollup/rollup-linux-s390x-gnu": "4.27.3", "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
"@rollup/rollup-linux-x64-gnu": "4.27.3", "@rollup/rollup-linux-s390x-gnu": "4.28.1",
"@rollup/rollup-linux-x64-musl": "4.27.3", "@rollup/rollup-linux-x64-gnu": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.27.3", "@rollup/rollup-linux-x64-musl": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.27.3", "@rollup/rollup-win32-arm64-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.27.3", "@rollup/rollup-win32-ia32-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -894,9 +917,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.36.0", "version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
deps/sqlite/shell.c vendored
View File

@ -5072,10 +5072,10 @@ int sqlite3_percentile_init(
){ ){
int rc = SQLITE_OK; int rc = SQLITE_OK;
unsigned int i; unsigned int i;
#if defined(SQLITE3_H) || defined(SQLITE_STATIC_PERCENTILE) #ifdef SQLITE3EXT_H
(void)pApi; /* Unused parameter */
#else
SQLITE_EXTENSION_INIT2(pApi); SQLITE_EXTENSION_INIT2(pApi);
#else
(void)pApi; /* Unused parameter */
#endif #endif
(void)pzErrMsg; /* Unused parameter */ (void)pzErrMsg; /* Unused parameter */
for(i=0; i<sizeof(aPercentFunc)/sizeof(aPercentFunc[0]); i++){ for(i=0; i<sizeof(aPercentFunc)/sizeof(aPercentFunc[0]); i++){

67
deps/sqlite/sqlite3.c vendored
View File

@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
** This file is an amalgamation of many separate C source files from SQLite ** This file is an amalgamation of many separate C source files from SQLite
** version 3.47.1. By combining all the individual C code files into this ** version 3.47.2. By combining all the individual C code files into this
** single large file, the entire code can be compiled as a single translation ** 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 ** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements ** 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. ** separate file. This file contains only code for the core SQLite library.
** **
** The content in this amalgamation comes from Fossil check-in ** The content in this amalgamation comes from Fossil check-in
** b95d11e958643b969c47a8e5857f3793b9e6. ** 2aabe05e2e8cae4847a802ee2daddc1d7413.
*/ */
#define SQLITE_CORE 1 #define SQLITE_CORE 1
#define SQLITE_AMALGAMATION 1 #define SQLITE_AMALGAMATION 1
@ -462,9 +462,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.47.1" #define SQLITE_VERSION "3.47.2"
#define SQLITE_VERSION_NUMBER 3047001 #define SQLITE_VERSION_NUMBER 3047002
#define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e" #define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -35697,8 +35697,8 @@ SQLITE_PRIVATE int sqlite3AtoF(const char *z, double *pResult, int length, u8 en
int eValid = 1; /* True exponent is either not used or is well-formed */ int eValid = 1; /* True exponent is either not used or is well-formed */
int nDigit = 0; /* Number of digits processed */ int nDigit = 0; /* Number of digits processed */
int eType = 1; /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */ int eType = 1; /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */
u64 s2; /* round-tripped significand */
double rr[2]; double rr[2];
u64 s2;
assert( enc==SQLITE_UTF8 || enc==SQLITE_UTF16LE || enc==SQLITE_UTF16BE ); assert( enc==SQLITE_UTF8 || enc==SQLITE_UTF16LE || enc==SQLITE_UTF16BE );
*pResult = 0.0; /* Default return value, in case of an error */ *pResult = 0.0; /* Default return value, in case of an error */
@ -35801,7 +35801,7 @@ do_atof_calc:
e = (e*esign) + d; e = (e*esign) + d;
/* Try to adjust the exponent to make it smaller */ /* Try to adjust the exponent to make it smaller */
while( e>0 && s<(LARGEST_UINT64/10) ){ while( e>0 && s<((LARGEST_UINT64-0x7ff)/10) ){
s *= 10; s *= 10;
e--; e--;
} }
@ -35811,11 +35811,16 @@ do_atof_calc:
} }
rr[0] = (double)s; rr[0] = (double)s;
assert( sizeof(s2)==sizeof(rr[0]) );
memcpy(&s2, &rr[0], sizeof(s2));
if( s2<=0x43efffffffffffffLL ){
s2 = (u64)rr[0]; s2 = (u64)rr[0];
#if defined(_MSC_VER) && _MSC_VER<1700
if( s2==0x8000000000000000LL ){ s2 = 2*(u64)(0.5*rr[0]); }
#endif
rr[1] = s>=s2 ? (double)(s - s2) : -(double)(s2 - s); rr[1] = s>=s2 ? (double)(s - s2) : -(double)(s2 - s);
}else{
rr[1] = 0.0;
}
assert( rr[1]<=1.0e-10*rr[0] ); /* Equal only when rr[0]==0.0 */
if( e>0 ){ if( e>0 ){
while( e>=100 ){ while( e>=100 ){
e -= 100; e -= 100;
@ -147605,32 +147610,32 @@ static Expr *substExpr(
if( pSubst->isOuterJoin ){ if( pSubst->isOuterJoin ){
ExprSetProperty(pNew, EP_CanBeNull); ExprSetProperty(pNew, EP_CanBeNull);
} }
if( pNew->op==TK_TRUEFALSE ){
pNew->u.iValue = sqlite3ExprTruthValue(pNew);
pNew->op = TK_INTEGER;
ExprSetProperty(pNew, EP_IntValue);
}
/* Ensure that the expression now has an implicit collation sequence,
** just as it did when it was a column of a view or sub-query. */
{
CollSeq *pNat = sqlite3ExprCollSeq(pSubst->pParse, pNew);
CollSeq *pColl = sqlite3ExprCollSeq(pSubst->pParse,
pSubst->pCList->a[iColumn].pExpr
);
if( pNat!=pColl || (pNew->op!=TK_COLUMN && pNew->op!=TK_COLLATE) ){
pNew = sqlite3ExprAddCollateString(pSubst->pParse, pNew,
(pColl ? pColl->zName : "BINARY")
);
}
}
ExprClearProperty(pNew, EP_Collate);
if( ExprHasProperty(pExpr,EP_OuterON|EP_InnerON) ){ if( ExprHasProperty(pExpr,EP_OuterON|EP_InnerON) ){
sqlite3SetJoinExpr(pNew, pExpr->w.iJoin, sqlite3SetJoinExpr(pNew, pExpr->w.iJoin,
pExpr->flags & (EP_OuterON|EP_InnerON)); pExpr->flags & (EP_OuterON|EP_InnerON));
} }
sqlite3ExprDelete(db, pExpr); sqlite3ExprDelete(db, pExpr);
pExpr = pNew; pExpr = pNew;
if( pExpr->op==TK_TRUEFALSE ){
pExpr->u.iValue = sqlite3ExprTruthValue(pExpr);
pExpr->op = TK_INTEGER;
ExprSetProperty(pExpr, EP_IntValue);
}
/* Ensure that the expression now has an implicit collation sequence,
** just as it did when it was a column of a view or sub-query. */
{
CollSeq *pNat = sqlite3ExprCollSeq(pSubst->pParse, pExpr);
CollSeq *pColl = sqlite3ExprCollSeq(pSubst->pParse,
pSubst->pCList->a[iColumn].pExpr
);
if( pNat!=pColl || (pExpr->op!=TK_COLUMN && pExpr->op!=TK_COLLATE) ){
pExpr = sqlite3ExprAddCollateString(pSubst->pParse, pExpr,
(pColl ? pColl->zName : "BINARY")
);
}
}
ExprClearProperty(pExpr, EP_Collate);
} }
} }
}else{ }else{
@ -254938,7 +254943,7 @@ static void fts5SourceIdFunc(
){ ){
assert( nArg==0 ); assert( nArg==0 );
UNUSED_PARAM2(nArg, apUnused); UNUSED_PARAM2(nArg, apUnused);
sqlite3_result_text(pCtx, "fts5: 2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e", -1, SQLITE_TRANSIENT); sqlite3_result_text(pCtx, "fts5: 2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c", -1, SQLITE_TRANSIENT);
} }
/* /*

View File

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.47.1" #define SQLITE_VERSION "3.47.2"
#define SQLITE_VERSION_NUMBER 3047001 #define SQLITE_VERSION_NUMBER 3047002
#define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e" #define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers

6
package-lock.json generated
View File

@ -11,9 +11,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.3.3", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"

View File

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

View File

@ -385,8 +385,11 @@ public class TildeFriendsActivity extends Activity {
public void onServiceConnected(ComponentName name, IBinder binder) { public void onServiceConnected(ComponentName name, IBinder binder) {
Log.w("tildefriends", "onServiceConnected"); Log.w("tildefriends", "onServiceConnected");
Parcel data = Parcel.obtain(); Parcel data = Parcel.obtain();
ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(pipe_fd); try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(pipe_fd)) {
data.writeParcelable(pfd, 0); data.writeParcelable(pfd, 0);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e);
}
try { try {
binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY); binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) { } catch (RemoteException e) {

View File

@ -1360,7 +1360,7 @@ static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
user_app_t* user_app = _parse_user_app_from_path(request->path, "/save"); user_app_t* user_app = _parse_user_app_from_path(request->path, "/save");
if (user_app) if (user_app)
{ {
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration"))) if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, user_string, "administration")))
{ {
size_t path_length = strlen("path:") + strlen(user_app->app) + 1; size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length); char* app_path = tf_malloc(path_length);
@ -1516,7 +1516,7 @@ static void _httpd_endpoint_delete_work(tf_ssb_t* ssb, void* user_data)
user_app_t* user_app = _parse_user_app_from_path(request->path, "/delete"); user_app_t* user_app = _parse_user_app_from_path(request->path, "/delete");
if (user_app) if (user_app)
{ {
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration"))) if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, user_string, "administration")))
{ {
size_t path_length = strlen("path:") + strlen(user_app->app) + 1; size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length); char* app_path = tf_malloc(path_length);

View File

@ -12,6 +12,8 @@ typedef struct _tf_packetstream_t
{ {
tf_packetstream_onreceive_t* onreceive; tf_packetstream_onreceive_t* onreceive;
void* onreceive_user_data; void* onreceive_user_data;
tf_packetstream_on_close_t* on_close;
void* on_close_user_data;
uv_pipe_t stream; uv_pipe_t stream;
char* buffer; char* buffer;
size_t buffer_size; size_t buffer_size;
@ -30,6 +32,8 @@ void tf_packetstream_destroy(tf_packetstream_t* stream)
{ {
stream->onreceive = NULL; stream->onreceive = NULL;
stream->onreceive_user_data = NULL; stream->onreceive_user_data = NULL;
stream->on_close = NULL;
stream->on_close_user_data = NULL;
stream->destroyed = true; stream->destroyed = true;
if (stream->buffer) if (stream->buffer)
{ {
@ -110,6 +114,14 @@ static void _packetstream_on_read(uv_stream_t* handle, ssize_t count, const uv_b
} }
else else
{ {
tf_packetstream_on_close_t* on_close = stream->on_close;
void* user_data = stream->on_close_user_data;
stream->on_close = NULL;
stream->on_close_user_data = NULL;
if (on_close)
{
on_close(user_data);
}
tf_packetstream_close(stream); tf_packetstream_close(stream);
} }
tf_free(buffer->base); tf_free(buffer->base);
@ -150,7 +162,7 @@ void tf_packetstream_send(tf_packetstream_t* stream, int packet_type, const char
int result = uv_write(request, (uv_stream_t*)&stream->stream, &write_buffer, 1, _packetstream_on_write); int result = uv_write(request, (uv_stream_t*)&stream->stream, &write_buffer, 1, _packetstream_on_write);
if (result) if (result)
{ {
tf_printf("uv_write: %s\n", uv_strerror(result)); tf_printf("tf_packetstream_send: uv_write: %s\n", uv_strerror(result));
tf_free(request); tf_free(request);
} }
} }
@ -162,6 +174,12 @@ void tf_packetstream_set_on_receive(tf_packetstream_t* stream, tf_packetstream_o
stream->onreceive_user_data = user_data; stream->onreceive_user_data = user_data;
} }
void tf_packetstream_set_on_close(tf_packetstream_t* stream, tf_packetstream_on_close_t* callback, void* user_data)
{
stream->on_close = callback;
stream->on_close_user_data = user_data;
}
static void _tf_packetstream_handle_closed(uv_handle_t* handle) static void _tf_packetstream_handle_closed(uv_handle_t* handle)
{ {
tf_packetstream_t* packetstream = handle->data; tf_packetstream_t* packetstream = handle->data;

View File

@ -23,6 +23,12 @@ typedef struct _tf_packetstream_t tf_packetstream_t;
*/ */
typedef void(tf_packetstream_onreceive_t)(int packet_type, const char* begin, size_t length, void* user_data); typedef void(tf_packetstream_onreceive_t)(int packet_type, const char* begin, size_t length, void* user_data);
/**
** A function called when a packetstream reads EOF.
** @param user_data User data.
*/
typedef void(tf_packetstream_on_close_t)(void* user_data);
/** /**
** Create a packet stream. ** Create a packet stream.
** @return The packet stream. ** @return The packet stream.
@ -58,6 +64,14 @@ void tf_packetstream_send(tf_packetstream_t* stream, int packet_type, const char
*/ */
void tf_packetstream_set_on_receive(tf_packetstream_t* stream, tf_packetstream_onreceive_t* callback, void* user_data); void tf_packetstream_set_on_receive(tf_packetstream_t* stream, tf_packetstream_onreceive_t* callback, void* user_data);
/**
** Register a callback for when a stream reads EOF.
** @param stream The packet stream.
** @param callback The callback.
** @param user_data User data to pass to the callback.
*/
void tf_packetstream_set_on_close(tf_packetstream_t* stream, tf_packetstream_on_close_t* callback, void* user_data);
/** /**
** Close a packet stream. ** Close a packet stream.
** @param stream The packet stream. ** @param stream The packet stream.

View File

@ -294,6 +294,7 @@ typedef struct _tf_ssb_connection_t
uv_tcp_t tcp; uv_tcp_t tcp;
uv_connect_t connect; uv_connect_t connect;
uv_async_t async; uv_async_t async;
uv_async_t scheduled_async;
uv_timer_t handshake_timer; uv_timer_t handshake_timer;
bool closing; bool closing;
@ -385,6 +386,7 @@ static bool _tf_ssb_parse_broadcast(const char* in_broadcast, tf_ssb_broadcast_t
static void _tf_ssb_start_update_settings(tf_ssb_t* ssb); static void _tf_ssb_start_update_settings(tf_ssb_t* ssb);
static void _tf_ssb_update_settings(tf_ssb_t* ssb); static void _tf_ssb_update_settings(tf_ssb_t* ssb);
static void _tf_ssb_write(tf_ssb_connection_t* connection, void* data, size_t size); static void _tf_ssb_write(tf_ssb_connection_t* connection, void* data, size_t size);
static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connection);
static const char* _tf_ssb_connection_state_to_string(tf_ssb_state_t state) static const char* _tf_ssb_connection_state_to_string(tf_ssb_state_t state)
{ {
@ -663,9 +665,15 @@ static void _tf_ssb_connection_box_stream_send(tf_ssb_connection_t* connection,
} }
} }
static void _tf_ssb_connection_scheduled_async(uv_async_t* async)
{
tf_ssb_connection_t* connection = async->data;
_tf_ssb_connection_dispatch_scheduled(connection);
}
static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connection) static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connection)
{ {
while ((connection->active_write_count == 0 || connection->closing) && connection->scheduled_count && connection->scheduled) while (((connection->active_write_count == 0 && connection->read_back_pressure == 0) || connection->closing) && connection->scheduled_count && connection->scheduled)
{ {
tf_ssb_connection_scheduled_t scheduled = connection->scheduled[0]; tf_ssb_connection_scheduled_t scheduled = connection->scheduled[0];
memmove(connection->scheduled, connection->scheduled + 1, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count - 1)); memmove(connection->scheduled, connection->scheduled + 1, sizeof(tf_ssb_connection_scheduled_t) * (connection->scheduled_count - 1));
@ -683,7 +691,7 @@ void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_sch
.callback = callback, .callback = callback,
.user_data = user_data, .user_data = user_data,
}; };
_tf_ssb_connection_dispatch_scheduled(connection); uv_async_send(&connection->scheduled_async);
} }
static int _request_compare(const void* a, const void* b) static int _request_compare(const void* a, const void* b)
@ -1988,6 +1996,10 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
{ {
uv_close((uv_handle_t*)&connection->async, _tf_ssb_connection_on_close); uv_close((uv_handle_t*)&connection->async, _tf_ssb_connection_on_close);
} }
if (connection->scheduled_async.data && !uv_is_closing((uv_handle_t*)&connection->scheduled_async))
{
uv_close((uv_handle_t*)&connection->scheduled_async, _tf_ssb_connection_on_close);
}
if (connection->tcp.data && !uv_is_closing((uv_handle_t*)&connection->tcp)) if (connection->tcp.data && !uv_is_closing((uv_handle_t*)&connection->tcp))
{ {
uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close); uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close);
@ -1997,8 +2009,8 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
uv_close((uv_handle_t*)&connection->handshake_timer, _tf_ssb_connection_on_close); uv_close((uv_handle_t*)&connection->handshake_timer, _tf_ssb_connection_on_close);
} }
if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->tcp.data && !connection->connect.data && !connection->handshake_timer.data && if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->scheduled_async.data && !connection->tcp.data && !connection->connect.data &&
connection->ref_count == 0) !connection->handshake_timer.data && connection->ref_count == 0)
{ {
tf_free(connection->message_requests); tf_free(connection->message_requests);
connection->message_requests = NULL; connection->message_requests = NULL;
@ -2792,6 +2804,8 @@ static tf_ssb_connection_t* _tf_ssb_connection_create(
connection->port = ntohs(addr->sin_port); connection->port = ntohs(addr->sin_port);
connection->async.data = connection; connection->async.data = connection;
uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async); uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async);
connection->scheduled_async.data = connection;
uv_async_init(ssb->loop, &connection->scheduled_async, _tf_ssb_connection_scheduled_async);
connection->connect_callback = callback; connection->connect_callback = callback;
connection->connect_callback_user_data = user_data; connection->connect_callback_user_data = user_data;
@ -2848,6 +2862,10 @@ static void _tf_ssb_connection_tunnel_callback(
tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id, int connect_flags) tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id, int connect_flags)
{ {
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, portal_id); tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, portal_id);
if (!connection)
{
return NULL;
}
JSContext* context = ssb->context; JSContext* context = ssb->context;
tf_ssb_connection_t* tunnel = tf_malloc(sizeof(tf_ssb_connection_t)); tf_ssb_connection_t* tunnel = tf_malloc(sizeof(tf_ssb_connection_t));
@ -2861,6 +2879,8 @@ tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char*
tunnel->send_request_number = 1; tunnel->send_request_number = 1;
tunnel->async.data = tunnel; tunnel->async.data = tunnel;
uv_async_init(ssb->loop, &tunnel->async, _tf_ssb_connection_process_message_async); uv_async_init(ssb->loop, &tunnel->async, _tf_ssb_connection_process_message_async);
tunnel->scheduled_async.data = tunnel;
uv_async_init(ssb->loop, &tunnel->scheduled_async, _tf_ssb_connection_scheduled_async);
tunnel->handshake_timer.data = tunnel; tunnel->handshake_timer.data = tunnel;
uv_timer_init(ssb->loop, &tunnel->handshake_timer); uv_timer_init(ssb->loop, &tunnel->handshake_timer);
@ -2998,6 +3018,8 @@ static void _tf_ssb_on_connection(uv_stream_t* stream, int status)
connection->send_request_number = 1; connection->send_request_number = 1;
connection->async.data = connection; connection->async.data = connection;
uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async); uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async);
connection->scheduled_async.data = connection;
uv_async_init(ssb->loop, &connection->scheduled_async, _tf_ssb_connection_scheduled_async);
connection->object = JS_NewObjectClass(ssb->context, _connection_class_id); connection->object = JS_NewObjectClass(ssb->context, _connection_class_id);
JS_SetOpaque(connection->object, connection); JS_SetOpaque(connection->object, connection);
@ -4376,6 +4398,7 @@ void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection,
const int k_threshold = 256; const int k_threshold = 256;
int old_pressure = connection->read_back_pressure; int old_pressure = connection->read_back_pressure;
connection->read_back_pressure += delta; connection->read_back_pressure += delta;
uv_async_send(&connection->scheduled_async);
if (!connection->closing) if (!connection->closing)
{ {
if (old_pressure < k_threshold && connection->read_back_pressure >= k_threshold) if (old_pressure < k_threshold && connection->read_back_pressure >= k_threshold)
@ -4397,7 +4420,7 @@ void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection,
void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta) void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta)
{ {
connection->active_write_count += delta; connection->active_write_count += delta;
_tf_ssb_connection_dispatch_scheduled(connection); uv_async_send(&connection->scheduled_async);
} }
void tf_ssb_sync_start(tf_ssb_t* ssb) void tf_ssb_sync_start(tf_ssb_t* ssb)
@ -4427,8 +4450,7 @@ bool tf_ssb_tunnel_create(tf_ssb_t* ssb, const char* portal_id, const char* targ
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_new_request, request_number, "tunnel.connect", message, NULL, NULL, NULL); tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_new_request, request_number, "tunnel.connect", message, NULL, NULL, NULL);
JS_FreeValue(context, message); JS_FreeValue(context, message);
tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id, connect_flags); return tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id, connect_flags) != NULL;
return true;
} }
return false; return false;
} }

View File

@ -135,6 +135,7 @@ 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_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_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_size_by_author_index ON messages (author, length(content))");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_author_channel_index ON messages (content ->> 'type', author, content ->> 'channel');");
_tf_ssb_db_exec(db, _tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs (" "CREATE TABLE IF NOT EXISTS blobs ("
" id TEXT PRIMARY KEY," " id TEXT PRIMARY KEY,"
@ -233,13 +234,21 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "DROP VIEW IF EXISTS blob_wants_view"); _tf_ssb_db_exec(db, "DROP VIEW IF EXISTS blob_wants_view");
_tf_ssb_db_exec(db, _tf_ssb_db_exec(db,
"CREATE VIEW IF NOT EXISTS blob_wants_view (id, timestamp) AS " "CREATE VIEW IF NOT EXISTS blob_wants_view (id, timestamp) AS "
" WITH wanted AS ( "
" SELECT messages_refs.ref AS id, messages.timestamp AS timestamp " " SELECT messages_refs.ref AS id, messages.timestamp AS timestamp "
" FROM messages_refs " " FROM messages_refs "
" JOIN messages ON messages.id = messages_refs.message " " JOIN messages ON messages.id = messages_refs.message "
" LEFT OUTER JOIN blobs ON messages_refs.ref = blobs.id " " UNION "
" SELECT messages_refs.ref AS id, unixepoch() * 1000 AS timestamp "
" FROM messages_refs "
" JOIN messages ON messages.id = messages_refs.message "
" WHERE messages.content ->> 'type' = 'about' "
" ) "
" SELECT wanted.id, wanted.timestamp FROM wanted "
" LEFT OUTER JOIN blobs ON wanted.id = blobs.id "
" WHERE blobs.id IS NULL " " WHERE blobs.id IS NULL "
" AND LENGTH(messages_refs.ref) = 52 " " AND LENGTH(wanted.id) = 52 "
" AND messages_refs.ref LIKE '&%.sha256'"); " AND wanted.id LIKE '&%.sha256'");
bool need_add_flags = true; bool need_add_flags = true;
bool need_convert_timestamp_to_real = false; bool need_convert_timestamp_to_real = false;
@ -1998,12 +2007,12 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
return verified; return verified;
} }
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission) bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, const char* permission)
{ {
bool has_permission = false; bool has_permission = false;
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* reader = db ? db : tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL; sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, if (sqlite3_prepare(reader,
"SELECT COUNT(*) FROM properties, json_each(properties.value -> 'permissions' -> ?) AS permission WHERE properties.id = 'core' AND properties.key = 'settings' AND " "SELECT COUNT(*) FROM properties, json_each(properties.value -> 'permissions' -> ?) AS permission WHERE properties.id = 'core' AND properties.key = 'settings' AND "
"permission.value = ?", "permission.value = ?",
-1, &statement, NULL) == SQLITE_OK) -1, &statement, NULL) == SQLITE_OK)
@ -2015,6 +2024,9 @@ bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* pe
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db); if (reader != db)
{
tf_ssb_release_db_reader(ssb, reader);
}
return has_permission; return has_permission;
} }

View File

@ -448,11 +448,12 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
/** /**
** Check if a user has a specific permission. ** Check if a user has a specific permission.
** @param ssb The SSB instance. ** @param ssb The SSB instance.
** @param db Optional database instance. If NULL, one will be acquired from ssb.
** @param id The user ID. ** @param id The user ID.
** @param permission The name of the permission. ** @param permission The name of the permission.
** @return true If the user has the requested permission. ** @return true If the user has the requested permission.
*/ */
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission); bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, const char* permission);
/** /**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.

View File

@ -368,9 +368,9 @@ typedef struct _swap_with_server_identity_t
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{ {
swap_with_server_identity_t* work = user_data; swap_with_server_identity_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, work->user, "administration"))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (tf_ssb_db_user_has_permission(ssb, db, work->user, "administration"))
{
char* error = NULL; char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK) if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{ {
@ -404,12 +404,12 @@ static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_dat
{ {
work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db)); work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
} }
tf_ssb_release_db_writer(ssb, db);
} }
else else
{ {
work->error = tf_strdup("not administrator"); 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) static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -480,7 +480,7 @@ static void _tf_ssb_getIdentities_visit(const char* identity, void* user_data)
static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{ {
identities_visit_t* work = user_data; identities_visit_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, work->user, "administration")) if (tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{ {
char id[k_id_base64_len] = ""; char id[k_id_base64_len] = "";
if (tf_ssb_whoami(ssb, id, sizeof(id))) if (tf_ssb_whoami(ssb, id, sizeof(id)))
@ -552,7 +552,7 @@ static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{ {
get_private_key_t* work = user_data; get_private_key_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key)); work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, work->user, "administration")) if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{ {
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key)); work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
} }
@ -658,7 +658,7 @@ static void _tf_ssb_getActiveIdentity_work(tf_ssb_t* ssb, void* user_data)
tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request); tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request);
} }
if (!*request->identity && tf_ssb_db_user_has_permission(ssb, request->name, "administration")) if (!*request->identity && tf_ssb_db_user_has_permission(ssb, NULL, request->name, "administration"))
{ {
tf_ssb_whoami(ssb, request->identity, sizeof(request->identity)); tf_ssb_whoami(ssb, request->identity, sizeof(request->identity));
} }
@ -742,7 +742,7 @@ static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
{ {
identity_info_work_t* request = user_data; identity_info_work_t* request = user_data;
char id[k_id_base64_len] = ""; char id[k_id_base64_len] = "";
if (tf_ssb_db_user_has_permission(ssb, request->name, "administration")) if (tf_ssb_db_user_has_permission(ssb, NULL, request->name, "administration"))
{ {
if (tf_ssb_whoami(ssb, id, sizeof(id))) if (tf_ssb_whoami(ssb, id, sizeof(id)))
{ {
@ -904,7 +904,7 @@ static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, voi
{ {
append_message_t* work = user_data; append_message_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key)); work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, work->user, "administration")) if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, NULL, work->user, "administration"))
{ {
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key)); work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
} }
@ -1974,7 +1974,7 @@ enum
k_max_private_message_recipients = 8 k_max_private_message_recipients = 8
}; };
static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES]) static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{ {
if (!user || !identity) if (!user || !identity)
{ {
@ -2003,6 +2003,21 @@ static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, co
return success; return success;
} }
static bool _tf_ssb_get_private_key_curve25519(tf_ssb_t* ssb, sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (_tf_ssb_get_private_key_curve25519_internal(db, user, identity, out_private_key))
{
return true;
}
if (tf_ssb_db_user_has_permission(ssb, db, user, "administration"))
{
return _tf_ssb_get_private_key_curve25519_internal(db, ":admin", identity, out_private_key);
}
return false;
}
typedef struct _private_message_encrypt_t typedef struct _private_message_encrypt_t
{ {
const char* signer_user; const char* signer_user;
@ -2025,7 +2040,7 @@ static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(db, work->signer_user, work->signer_identity, private_key); bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->signer_user, work->signer_identity, private_key);
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
if (found) if (found)
@ -2214,7 +2229,7 @@ static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 }; uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool found = _tf_ssb_get_private_key_curve25519(db, work->user, work->identity, private_key); bool found = _tf_ssb_get_private_key_curve25519(ssb, db, work->user, work->identity, private_key);
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
if (found) if (found)
@ -2341,7 +2356,7 @@ typedef struct _following_t
static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data)
{ {
following_t* following = user_data; following_t* following = user_data;
following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth - 1); following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth);
} }
static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data)

View File

@ -816,8 +816,7 @@ static void _tf_ssb_connection_send_history_stream_work(tf_ssb_connection_t* con
const int k_max = 32; const int k_max = 32;
if (sqlite3_prepare(db, if (sqlite3_prepare(db,
"SELECT previous, author, id, sequence, timestamp, hash, json(content), signature, flags FROM messages WHERE author = ?1 AND sequence > ?2 AND " "SELECT previous, author, id, sequence, timestamp, hash, json(content), signature, flags FROM messages WHERE author = ?1 AND sequence > ?2 AND "
"sequence " "sequence < ?3 ORDER BY sequence",
"< ?3 ORDER BY sequence",
-1, &statement, NULL) == SQLITE_OK) -1, &statement, NULL) == SQLITE_OK)
{ {
if (sqlite3_bind_text(statement, 1, request->author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, request->sequence) == SQLITE_OK && if (sqlite3_bind_text(statement, 1, request->author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, request->sequence) == SQLITE_OK &&
@ -828,7 +827,8 @@ static void _tf_ssb_connection_send_history_stream_work(tf_ssb_connection_t* con
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL); JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime); JSContext* context = JS_NewContext(runtime);
while (sqlite3_step(statement) == SQLITE_ROW) int r = SQLITE_OK;
while ((r = sqlite3_step(statement)) == SQLITE_ROW)
{ {
JSValue message = JS_UNDEFINED; JSValue message = JS_UNDEFINED;
request->out_max_sequence_seen = sqlite3_column_int64(statement, 3); request->out_max_sequence_seen = sqlite3_column_int64(statement, 3);
@ -1003,10 +1003,10 @@ static void _tf_ssb_rpc_ebt_replicate_send_clock_work(tf_ssb_connection_t* conne
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSValue full_clock = JS_NewObject(context); JSValue full_clock = JS_NewObject(context);
int64_t depth = _get_global_setting_int64(ssb, "replication_hops", -1); int64_t depth = _get_global_setting_int64(ssb, "replication_hops", 2);
/* Ask for every identity we know is being followed from local accounts. */ /* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth - 1); const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
for (int i = 0; visible[i]; i++) for (int i = 0; visible[i]; i++)
{ {
int64_t sequence = 0; int64_t sequence = 0;
@ -1173,6 +1173,20 @@ static void _tf_ssb_rpc_ebt_replicate_store_callback(const char* id, bool verifi
tf_ssb_connection_adjust_read_backpressure(connection, -1); tf_ssb_connection_adjust_read_backpressure(connection, -1);
} }
typedef struct _resend_clock_t
{
tf_ssb_connection_t* connection;
int32_t request_number;
} resend_clock_t;
static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, void* user_data)
{
resend_clock_t* resend = user_data;
_tf_ssb_rpc_ebt_replicate_send_clock(resend->connection, resend->request_number, JS_UNDEFINED);
tf_ssb_connection_set_sent_clock(resend->connection, true);
tf_free(user_data);
}
static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{ {
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
@ -1198,6 +1212,17 @@ static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t f
/* Looks like a message. */ /* Looks like a message. */
tf_ssb_connection_adjust_read_backpressure(connection, 1); tf_ssb_connection_adjust_read_backpressure(connection, 1);
tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection); tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection);
if (tf_ssb_connection_get_sent_clock(connection))
{
tf_ssb_connection_set_sent_clock(connection, false);
resend_clock_t* resend = tf_malloc(sizeof(resend_clock_t));
*resend = (resend_clock_t) {
.connection = connection,
.request_number = request_number,
};
tf_ssb_connection_schedule_idle(connection, _tf_ssb_rpc_ebt_replicate_resend_clock, resend);
}
} }
else else
{ {

View File

@ -16,6 +16,8 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#include "sodium/crypto_sign.h"
#if !defined(_WIN32) #if !defined(_WIN32)
#include <sys/wait.h> #include <sys/wait.h>
#endif #endif
@ -900,8 +902,7 @@ static void _write_file(const char* path, const char* contents)
FILE* file = fopen(path, "w"); FILE* file = fopen(path, "w");
if (!file) if (!file)
{ {
printf("Unable to write %s: %s.\n", path, strerror(errno)); tf_printf("Unable to write %s: %s.\n", path, strerror(errno));
fflush(stdout);
abort(); abort();
} }
fputs(contents, file); fputs(contents, file);
@ -931,7 +932,7 @@ void tf_ssb_test_encrypt(const tf_test_options_t* options)
int result = system(command); int result = system(command);
(void)result; (void)result;
assert(WIFEXITED(result)); assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0); assert(WEXITSTATUS(result) == 0);
} }
@ -1026,17 +1027,198 @@ void tf_ssb_test_publish(const tf_test_options_t* options)
int result = system(command); int result = system(command);
(void)result; (void)result;
assert(WIFEXITED(result)); assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0); assert(WEXITSTATUS(result) == 0);
snprintf(command, sizeof(command), "%s publish -d out/test_db0.sqlite -u :admin -i %s -c '{\"type\": \"post\", \"text\": \"Two.\"}'", executable, id); snprintf(command, sizeof(command), "%s publish -d out/test_db0.sqlite -u :admin -i %s -c '{\"type\": \"post\", \"text\": \"Two.\"}'", executable, id);
result = system(command); result = system(command);
assert(WIFEXITED(result)); assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0); assert(WEXITSTATUS(result) == 0);
uv_run(&loop, UV_RUN_DEFAULT); uv_run(&loop, UV_RUN_DEFAULT);
uv_loop_close(&loop); uv_loop_close(&loop);
} }
static void _test_print_identity(const char* identity, void* user_data)
{
tf_ssb_t* ssb = user_data;
int64_t sequence = -1;
char id[k_id_base64_len] = { 0 };
snprintf(id, sizeof(id), "@%s", identity);
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
tf_printf("IDENTITY %s: %d\n", id, (int)sequence);
}
void tf_ssb_test_replicate(const tf_test_options_t* options)
{
tf_printf("Testing replication.\n");
uv_loop_t loop = { 0 };
uv_loop_init(&loop);
unlink("out/test_db0.sqlite");
tf_ssb_t* ssb0 = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb0), ssb0);
unlink("out/test_db1.sqlite");
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, "file:out/test_db1.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
uv_idle_t idle0 = { .data = ssb0 };
uv_idle_init(&loop, &idle0);
uv_idle_start(&idle0, _ssb_test_idle);
uv_idle_t idle1 = { .data = ssb1 };
uv_idle_init(&loop, &idle1);
uv_idle_start(&idle1, _ssb_test_idle);
test_t test = {
.ssb0 = ssb0,
.ssb1 = ssb1,
};
tf_ssb_add_connections_changed_callback(ssb0, _ssb_test_connections_changed, NULL, &test);
tf_ssb_add_connections_changed_callback(ssb1, _ssb_test_connections_changed, NULL, &test);
tf_ssb_generate_keys(ssb0);
tf_ssb_generate_keys(ssb1);
uint8_t priv0[crypto_sign_SECRETKEYBYTES] = { 0 };
uint8_t priv1[crypto_sign_SECRETKEYBYTES] = { 0 };
tf_ssb_get_private_key(ssb0, priv0, sizeof(priv0));
tf_ssb_get_private_key(ssb1, priv1, sizeof(priv1));
char id0[k_id_base64_len] = { 0 };
char id1[k_id_base64_len] = { 0 };
bool b = tf_ssb_whoami(ssb0, id0, sizeof(id0));
(void)b;
assert(b);
b = tf_ssb_whoami(ssb1, id1, sizeof(id1));
assert(b);
tf_printf("ID %s and %s\n", id0, id1);
char priv0_str[512] = { 0 };
char priv1_str[512] = { 0 };
tf_base64_encode(priv0, sizeof(priv0), priv0_str, sizeof(priv0_str));
tf_base64_encode(priv1, sizeof(priv0), priv1_str, sizeof(priv1_str));
tf_ssb_db_identity_add(ssb0, "test", id0 + 1, priv0_str);
tf_ssb_db_identity_add(ssb1, "test", id1 + 1, priv1_str);
static const int k_key_count = 5;
char public[k_key_count][k_id_base64_len - 1];
char private[k_key_count][512];
for (int i = 0; i < k_key_count; i++)
{
tf_ssb_generate_keys_buffer(public[i], sizeof(public[i]), private[i], sizeof(private[i]));
bool added = tf_ssb_db_identity_add(ssb0, "test", public[i], private[i]);
tf_printf("%s user %d = %s private=%s\n", added ? "added" : "failed", i, public[i], private[i]);
}
JSContext* context0 = tf_ssb_get_context(ssb0);
for (int i = 0; i < k_key_count - 1; i++)
{
JSValue obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "contact"));
char self[k_id_base64_len];
snprintf(self, sizeof(self), "@%s", public[i]);
char contact[k_id_base64_len];
snprintf(contact, sizeof(contact), "@%s", public[i + 1]);
JS_SetPropertyStr(context0, obj, "contact", JS_NewString(context0, contact));
JS_SetPropertyStr(context0, obj, "following", JS_TRUE);
bool stored = false;
uint8_t private_bin[512] = { 0 };
tf_base64_decode(private[i], strlen(private[i]) - strlen(".ed25519"), private_bin, sizeof(private_bin));
tf_printf("ssb0 %s following %s\n", self, contact);
JSValue signed_message = tf_ssb_sign_message(ssb0, self, private_bin, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
obj = JS_NewObject(context0);
JS_SetPropertyStr(context0, obj, "type", JS_NewString(context0, "post"));
JS_SetPropertyStr(context0, obj, "text", JS_NewString(context0, "Hello, world!"));
stored = false;
signed_message = tf_ssb_sign_message(ssb0, self, private_bin, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb0, signed_message, _message_stored, &stored);
JS_FreeValue(context0, signed_message);
_wait_stored(ssb0, &stored);
JS_FreeValue(context0, obj);
}
JSContext* context1 = tf_ssb_get_context(ssb1);
{
JSValue obj = JS_NewObject(context1);
JS_SetPropertyStr(context1, obj, "type", JS_NewString(context1, "contact"));
char self[k_id_base64_len];
snprintf(self, sizeof(self), "%s", id1);
char contact[k_id_base64_len];
snprintf(contact, sizeof(contact), "@%s", public[0]);
JS_SetPropertyStr(context1, obj, "contact", JS_NewString(context1, contact));
JS_SetPropertyStr(context1, obj, "following", JS_TRUE);
bool stored = false;
tf_printf("ssb1 %s following %s\n", self, contact);
JSValue signed_message = tf_ssb_sign_message(ssb1, self, priv1, obj, NULL, 0);
tf_ssb_verify_strip_and_store_message(ssb1, signed_message, _message_stored, &stored);
JS_FreeValue(context1, signed_message);
_wait_stored(ssb1, &stored);
JS_FreeValue(context1, obj);
}
tf_printf("ssb0\n");
tf_ssb_db_identity_visit_all(ssb0, _test_print_identity, ssb0);
tf_printf("ssb1\n");
tf_ssb_db_identity_visit_all(ssb1, _test_print_identity, ssb1);
tf_ssb_server_open(ssb0, 12347);
uint8_t id0bin[k_id_bin_len];
tf_ssb_id_str_to_bin(id0bin, id0);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0, NULL, NULL);
tf_printf("Waiting for connection.\n");
while (test.connection_count0 != 1 || test.connection_count1 != 1)
{
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_ONCE);
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
}
tf_ssb_server_close(ssb0);
int count1 = 0;
tf_ssb_add_message_added_callback(ssb1, _message_added, NULL, &count1);
tf_printf("Waiting for message from other.\n");
while (count1 != 4)
{
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_ONCE);
tf_ssb_set_main_thread(ssb1, false);
}
tf_ssb_remove_message_added_callback(ssb1, _message_added, &count1);
tf_printf("done\n");
tf_ssb_send_close(ssb1);
uv_close((uv_handle_t*)&idle0, NULL);
uv_close((uv_handle_t*)&idle1, NULL);
tf_printf("final run\n");
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
uv_run(&loop, UV_RUN_DEFAULT);
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
tf_printf("done\n");
tf_printf("destroy 0\n");
tf_ssb_destroy(ssb0);
tf_printf("destroy 1\n");
tf_ssb_destroy(ssb1);
tf_printf("close\n");
uv_loop_close(&loop);
}
#endif #endif

View File

@ -65,4 +65,10 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options);
*/ */
void tf_ssb_test_publish(const tf_test_options_t* options); void tf_ssb_test_publish(const tf_test_options_t* options);
/**
** Test replication.
** @param options The test options.
*/
void tf_ssb_test_replicate(const tf_test_options_t* options);
/** @} */ /** @} */

View File

@ -1338,8 +1338,7 @@ void tf_task_resolve_promise(tf_task_t* task, promiseid_t promise, JSValue value
} }
else else
{ {
tf_printf("Didn't find promise %d to resolve.\n", promise); tf_printf("WARNING: Didn't find promise %d to resolve.\n", promise);
abort();
} }
} }
@ -1368,8 +1367,7 @@ void tf_task_reject_promise(tf_task_t* task, promiseid_t promise, JSValue value)
} }
else else
{ {
tf_printf("Didn't find promise %d to reject.\n", promise); tf_printf("WARNING: Didn't find promise %d to reject.\n", promise);
abort();
} }
} }

View File

@ -62,6 +62,7 @@ static JSValue _taskstub_set_on_print(JSContext* context, JSValueConst this_val,
static JSValue _taskstub_loadFile(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _taskstub_loadFile(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static void _taskstub_on_process_exit(uv_process_t* process, int64_t status, int terminationSignal); static void _taskstub_on_process_exit(uv_process_t* process, int64_t status, int terminationSignal);
static void _taskstub_finalizer(JSRuntime* runtime, JSValue value); static void _taskstub_finalizer(JSRuntime* runtime, JSValue value);
static void _taskstub_cleanup(tf_taskstub_t* stub);
static void _tf_taskstub_run_sandbox_thread(void* data) static void _tf_taskstub_run_sandbox_thread(void* data)
{ {
@ -74,12 +75,60 @@ static void _tf_taskstub_run_sandbox_thread(void* data)
tf_task_destroy(task); tf_task_destroy(task);
} }
static void _taskstub_on_handle_close(uv_handle_t* handle)
{
tf_taskstub_t* stub = handle->data;
tf_task_remove_child(stub->_owner, stub);
handle->data = NULL;
_taskstub_cleanup(stub);
}
static void _tf_taskstub_on_exit(tf_taskstub_t* stub, int64_t status, int termination_signal)
{
JSContext* context = tf_task_get_context(stub->_owner);
if (!JS_IsUndefined(stub->_on_exit))
{
JSValue ref = JS_DupValue(context, stub->_on_exit);
JSValue argv[] = { JS_NewInt64(context, status), JS_NewInt32(context, termination_signal) };
JSValue result = JS_Call(context, stub->_on_exit, JS_NULL, 2, argv);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, argv[0]);
JS_FreeValue(context, argv[1]);
JS_FreeValue(context, ref);
}
if (stub->_stream)
{
tf_packetstream_destroy(stub->_stream);
stub->_stream = NULL;
}
tf_task_remove_child(stub->_owner, stub);
if (stub->_process.data)
{
uv_close((uv_handle_t*)&stub->_process, _taskstub_on_handle_close);
}
else
{
_taskstub_cleanup(stub);
}
}
static void _tf_taskstub_packetstream_close(void* user_data)
{
tf_taskstub_t* stub = user_data;
if (!stub->_process.data)
{
_tf_taskstub_on_exit(stub, -1, -1);
}
}
static JSValue _taskstub_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _taskstub_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
tf_task_t* parent = tf_task_get(context); tf_task_t* parent = tf_task_get(context);
tf_taskstub_t* stub = tf_malloc(sizeof(tf_taskstub_t)); tf_taskstub_t* stub = tf_malloc(sizeof(tf_taskstub_t));
memset(stub, 0, sizeof(*stub)); memset(stub, 0, sizeof(*stub));
stub->_stream = tf_packetstream_create(); stub->_stream = tf_packetstream_create();
tf_packetstream_set_on_close(stub->_stream, _tf_taskstub_packetstream_close, stub);
JSValue taskObject = JS_NewObjectClass(context, _classId); JSValue taskObject = JS_NewObjectClass(context, _classId);
JS_SetOpaque(taskObject, stub); JS_SetOpaque(taskObject, stub);
@ -314,35 +363,10 @@ static void _taskstub_finalizer(JSRuntime* runtime, JSValue value)
_taskstub_cleanup(stub); _taskstub_cleanup(stub);
} }
static void _taskstub_on_handle_close(uv_handle_t* handle) static void _taskstub_on_process_exit(uv_process_t* process, int64_t status, int termination_signal)
{
tf_taskstub_t* stub = handle->data;
tf_task_remove_child(stub->_owner, stub);
handle->data = NULL;
_taskstub_cleanup(stub);
}
static void _taskstub_on_process_exit(uv_process_t* process, int64_t status, int terminationSignal)
{ {
tf_taskstub_t* stub = process->data; tf_taskstub_t* stub = process->data;
JSContext* context = tf_task_get_context(stub->_owner); _tf_taskstub_on_exit(stub, status, termination_signal);
if (!JS_IsUndefined(stub->_on_exit))
{
JSValue ref = JS_DupValue(context, stub->_on_exit);
JSValue argv[] = { JS_NewInt64(context, status), JS_NewInt32(context, terminationSignal) };
JSValue result = JS_Call(context, stub->_on_exit, JS_NULL, 2, argv);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, argv[0]);
JS_FreeValue(context, argv[1]);
JS_FreeValue(context, ref);
}
if (stub->_stream)
{
tf_packetstream_destroy(stub->_stream);
stub->_stream = NULL;
}
uv_close((uv_handle_t*)process, _taskstub_on_handle_close);
} }
static JSValue _taskstub_getExports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _taskstub_getExports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)

View File

@ -40,8 +40,7 @@ static void _write_file(const char* path, const char* contents)
FILE* file = fopen(path, "w"); FILE* file = fopen(path, "w");
if (!file) if (!file)
{ {
printf("Unable to write %s: %s.\n", path, strerror(errno)); tf_printf("Unable to write %s: %s.\n", path, strerror(errno));
fflush(stdout);
abort(); abort();
} }
fputs(contents, file); fputs(contents, file);
@ -1062,6 +1061,7 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false); _tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false); _tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
_tf_test_run(options, "publish", tf_ssb_test_publish, false); _tf_test_run(options, "publish", tf_ssb_test_publish, false);
_tf_test_run(options, "replicate", tf_ssb_test_replicate, false);
tf_printf("Tests completed.\n"); tf_printf("Tests completed.\n");
#endif #endif
} }

View File

@ -401,6 +401,11 @@ void tf_util_register(JSContext* context)
int tf_util_get_length(JSContext* context, JSValue value) int tf_util_get_length(JSContext* context, JSValue value)
{ {
if (JS_IsUndefined(value))
{
return 0;
}
JSValue length = JS_GetPropertyStr(context, value, "length"); JSValue length = JS_GetPropertyStr(context, value, "length");
int result = 0; int result = 0;
JS_ToInt32(context, &result, length); JS_ToInt32(context, &result, length);

View File

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

View File

@ -9,14 +9,59 @@ if sys.platform == 'haiku1':
print('Automation tests are disabled on Haiku.') print('Automation tests are disabled on Haiku.')
exit(0) exit(0)
import selenium
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.firefox.service import Service from selenium.webdriver.firefox.service import Service
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
def exists_in_shadow_root(shadow_root, by, value): def select(driver, path, action = None, keep_trying = False):
return lambda driver: shadow_root.find_element(by, value) start_time = time.time()
done = False
while True:
try:
driver.switch_to.default_content()
context = driver
for node in path:
if node.startswith('#'):
context = context.find_element(By.ID, node[1:])
elif node.startswith('/'):
context = context.find_element(By.XPATH, node)
elif node.startswith('.'):
context = context.find_element(By.CLASS_NAME, node[1:])
elif node.startswith('='):
context = context.find_element(By.LINK_TEXT, node[1:])
elif node == 'frame':
driver.switch_to.frame(context)
context = driver
elif node == 'shadow_root':
context = context.shadow_root
else:
context = context.find_element(By.TAG_NAME, node)
if action is not None:
if action[0] == 'click':
context.click()
elif action[0] == 'send_keys':
context.send_keys(action[1])
elif action[0] == 'clear':
context.clear()
else:
raise RuntimeError(f'Unexpected action: {action}.')
done = True
if not keep_trying:
break
else:
return context
except (selenium.common.exceptions.NoSuchElementException,
selenium.common.exceptions.NoSuchShadowRootException,
selenium.common.exceptions.StaleElementReferenceException,
selenium.common.exceptions.WebDriverException):
if done and keep_trying:
break
if time.time() - start_time < 5.0:
time.sleep(0.1)
pass
success = False success = False
try: try:
@ -27,290 +72,177 @@ try:
wait = WebDriverWait(driver, 10) wait = WebDriverWait(driver, 10)
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click() select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click() select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('adminuser') select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'adminuser'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('admin_password') select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'admin_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('admin_password') select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'admin_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#document', 'frame', '=identity'])
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
driver.switch_to.default_content()
driver.get('http://localhost:8888/~core/admin/') driver.get('http://localhost:8888/~core/admin/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#document', 'frame', '#gs_room_name'], ('send_keys', 'test room'))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) select(driver, ['#document', 'frame', '//*[@id="gs_room_name"]/following-sibling::button'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'gs_room_name'))).send_keys('test room') select(driver, ['//button[text()="✅ Allow"]'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//*[@id="gs_room_name"]/following-sibling::button'))).click()
driver.switch_to.alert.accept() driver.switch_to.alert.accept()
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click() select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click() select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click() select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click() select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password') select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password') select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'test_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
select(driver, ['#document'])
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['tf-navigation', 'shadow_root', '#create_identity'], ('click',))
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'create_identity').click()
wait.until(expected_conditions.alert_is_present()).accept() wait.until(expected_conditions.alert_is_present()).accept()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#id_dropdown', '//button[position()=2]'], ('click',))
driver.switch_to.default_content() select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '.tf-profile', 'shadow_root', '#edit_profile'], ('click',), keep_trying = True)
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click() select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '.tf-profile', 'shadow_root', '#name'], ('send_keys', 'user'))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'id_dropdown').find_element(By.XPATH, '//button[position()=2]').click() select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '.tf-profile', 'shadow_root', '#save_profile'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) select(driver, ['//button[text()="✅ Allow"]'], ('click',))
# NoSuchShadowRootException
while True:
try:
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
break
except:
pass
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'edit_profile').click()
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'name').send_keys('user')
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'save_profile').click()
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="✅ Allow"]'))).click()
driver.switch_to.default_content()
driver.get('http://localhost:8888/~testuser/test/') driver.get('http://localhost:8888/~testuser/test/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#document'])
while True: select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))
try: select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',))
wait.until(exists_in_shadow_root(driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root, By.ID, 'close_error')).click() select(driver, ['#editor', '.cm-content'], ('click',))
break select(driver, ['#editor', '.cm-content'], ('send_keys', 'app.setDocument(\n\t"<div id=\'test-div\'>Hello, world!</div>"\n);'))
except: select(driver, ['#save'], ('click',))
pass
driver.switch_to.default_content() select(driver, ['#document', 'frame', '#test-div'])
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
editor.click()
editor.send_keys('app.setDocument(\n\t"<div id=\'test-div\'>Hello, world!</div>"\n);');
driver.find_element(By.ID, 'save').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div')))
size = driver.get_window_size() size = driver.get_window_size()
driver.set_window_size(1200, 540) driver.set_window_size(1200, 540)
driver.save_screenshot('out/screenshot0.png') driver.save_screenshot('out/screenshot0.png')
driver.set_window_size(size['width'], size['height']) driver.set_window_size(size['width'], size['height'])
driver.switch_to.default_content() select(driver, ['#editor', '.cm-content'], ('click',))
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content') select(driver, ['#editor', '.cm-content'], ('clear',))
editor.click() select(driver, ['#editor', '.cm-content'], ('send_keys', 'app.setDocument("<div id=\'test-div2\'>Hello, world, again!</div>")'))
editor.clear() select(driver, ['#save'], ('click',))
editor.send_keys('app.setDocument("<div id=\'test-div2\'>Hello, world, again!</div>")'); select(driver, ['#document', 'frame', '#test-div2'])
driver.find_element(By.ID, 'save').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#delete'], ('click',))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div2')))
driver.switch_to.default_content()
driver.find_element(By.ID, 'delete').click()
wait.until(expected_conditions.alert_is_present()).accept() wait.until(expected_conditions.alert_is_present()).accept()
wait.until(expected_conditions.alert_is_present()).dismiss() wait.until(expected_conditions.alert_is_present()).dismiss()
driver.get('http://localhost:8888/~testuser/test/') driver.get('http://localhost:8888/~testuser/test/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
while True: select(driver, ['#document'])
try: select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))
wait.until(exists_in_shadow_root(driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root, By.ID, 'close_error')).click() select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',))
break
except:
pass
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#document', 'frame', '=identity'])
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
size = driver.get_window_size() size = driver.get_window_size()
driver.set_window_size(540, 1200) driver.set_window_size(540, 1200)
driver.save_screenshot('out/screenshot1.png') driver.save_screenshot('out/screenshot1.png')
driver.set_window_size(size['width'], size['height']) driver.set_window_size(size['width'], size['height'])
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity'))).click() select(driver, ['#document', 'frame', '=identity'], ('click',))
# StaleElementReferenceException select(driver, ['#document', 'frame', '#create_id'], ('click',))
while True: id0 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
try:
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'create_id'))).click()
driver.switch_to.alert.accept()
break
except:
pass
# StaleElementReferenceException select(driver, ['#document', 'frame', '//li/button[text()="Export Identity"]'], ('click',))
while True: select(driver, ['//button[text()="✅ Allow"]'], ('click',))
try: words = select(driver, ['#document', 'frame', '//li//textarea']).get_attribute('value')
id0 = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'li'))).text.split(' ')[-1] select(driver, ['#document', 'frame', '//li/button[text()="Delete Identity"]'], ('click',))
break
except:
pass
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//li/button[text()="Export Identity"]'))).click()
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="✅ Allow"]'))).click()
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
words = wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//li//textarea'))).get_attribute('value')
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//li/button[text()="Delete Identity"]'))).click()
driver.switch_to.alert.send_keys('DELETE') driver.switch_to.alert.send_keys('DELETE')
driver.switch_to.alert.accept() driver.switch_to.alert.accept()
driver.switch_to.default_content() select(driver, ['//button[text()="✅ Allow"]'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="✅ Allow"]'))).click() driver.switch_to.alert.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() driver.switch_to.alert.accept()
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
words = wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//textarea'))).send_keys(words) id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="Import Identity"]'))).click() assert id0 == id1
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="✅ Allow"]'))).click()
driver.switch_to.alert.accept()
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
id1 = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'li'))).text.split(' ')[-1]
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) select(driver, ['#document', 'frame', '=ssb'], ('click',))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'Hello, world!'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'ssb'))).click() select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))
driver.switch_to.default_content() select(driver, ['//button[text()="✅ Allow"]'], ('click',))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#login_label'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
# StaleElementReferenceException select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
while True: select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
try: select(driver, ['tf-auth', 'shadow_root', '#guest_label'], ('click',))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) select(driver, ['tf-auth', 'shadow_root', '#guestButton'], ('click',))
break select(driver, ['#document', 'frame', 'tf-app', 'shadow_root'])
except:
pass
# NoSuchShadowRootException
while True:
try:
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
break
except:
pass
# WebDriverException (shadow root is detached) select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
while True: select(driver, ['tf-auth', 'shadow_root', '#login_label'], ('click',))
try:
tf_tab_news = wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root
tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'edit').send_keys('Hello, world!')
tf_tab_news.find_element(By.ID, 'tf-compose').shadow_root.find_element(By.ID, 'submit').click()
break
except:
pass
driver.switch_to.default_content() select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
driver.find_element(By.ID, 'allow').click() select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'wrong_password'))
select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#error'])
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click() select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'wrong_user'))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click() select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click() select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') select(driver, ['tf-auth', 'shadow_root', '#error'])
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'wrong_test_password'))
select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'wrong_test_password'))
select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#error'])
while True: select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
try: select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click() select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
break select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'test_password'))
except: select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
pass select(driver, ['tf-auth', 'shadow_root', '#error'])
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guest_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'guestButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content'))) select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))) select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', '😁'))
# NoSuchShadowRootException select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
while True: select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'test_password'))
try: select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root select(driver, ['tf-auth', 'shadow_root', '#error'])
break
except:
pass
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click() select(driver, ['tf-auth', 'shadow_root', '#change_label'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click() select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
select(driver, ['tf-auth', 'shadow_root', '#new_password'], ('send_keys', 'new_password'))
select(driver, ['tf-auth', 'shadow_root', '#confirm'], ('send_keys', 'new_password'))
select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
select(driver, ['#document', 'frame'])
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_password') select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() select(driver, ['tf-auth', 'shadow_root', '#login_label'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error') select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'test_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('wrong_user') select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password') select(driver, ['tf-auth', 'shadow_root', '#error'])
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() select(driver, ['tf-auth', 'shadow_root', '#login_label'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error') select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
select(driver, ['tf-auth', 'shadow_root', '#password'], ('send_keys', 'new_password'))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click() select(driver, ['tf-auth', 'shadow_root', '#loginButton'], ('click',))
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser') select(driver, ['#document', 'frame'])
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('wrong_test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('wrong_test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('1invalid')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('😁')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'change_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'new_password').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
while True:
try:
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
break
except:
pass
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'error')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'login_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('testuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('new_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
success = True success = True
finally: finally:

View File

@ -34,6 +34,11 @@ def get_entries():
if m: if m:
results.append((time.mktime(entry.get('updated_parsed')), name, entry.link, f'new issue #{m.group(1)}: {m.group(2)}')) results.append((time.mktime(entry.get('updated_parsed')), name, entry.link, f'new issue #{m.group(1)}: {m.group(2)}'))
continue continue
elif '/releases/' in entry.link:
m = re.match(r'(.*) released <a href=".*?">(.*?)</a>', entry.title)
if m:
results.append((time.mktime(entry.get('updated_parsed')), name, entry.link, f'{m.group(1)} released {m.group(2)}'))
continue
if entry.summary.startswith('<a href='): if entry.summary.startswith('<a href='):
for m in re.findall(r'<a href="(.*?)">.*?</a>$\s*^([^\n]+)$', entry.summary, re.S | re.M): for m in re.findall(r'<a href="(.*?)">.*?</a>$\s*^([^\n]+)$', entry.summary, re.S | re.M):
results.append((time.mktime(entry.get('updated_parsed')), name, m[0], m[1])) results.append((time.mktime(entry.get('updated_parsed')), name, m[0], m[1]))

30
tools/emojis.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
import json
import os
import re
import urllib.request
if not os.path.exists('out/emoji-test.txt'):
urllib.request.urlretrieve('https://unicode.org/Public/emoji/latest/emoji-test.txt', 'out/emoji-test.txt')
doc = {}
with open('out/emoji-test.txt', 'r') as f:
for line in f:
line = line.strip()
if line.startswith('# group: '):
group = line[len('# group: '):]
elif line.startswith('# subgroup: '):
subgroup = line[len('# subgroup: '):]
else:
m = re.match(r'((?:\s?[0-9A-F]+)+)\s+;.*#.*E\d+\.\d+ (.*)', line)
if m:
emoji = ''.join(chr(int(g, 16)) for g in m.group(1).split(' '))
name = m.group(2)
if not group in doc:
doc[group] = {}
doc[group][name] = emoji
with open('apps/ssb/emojis.json', 'w') as f:
json.dump(doc, f, ensure_ascii = False)