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.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 30
VERSION_NUMBER := 0.0.25
VERSION_CODE := 31
VERSION_NUMBER := 0.0.26-wip
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
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool

View File

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

View File

@ -7,7 +7,7 @@ function textNode(text) {
function linkNode(text, link) {
const linkNode = new commonmark.Node('link', undefined);
if (link.startsWith('#')) {
linkNode.destination = `#q=${encodeURIComponent(link)}`;
linkNode.destination = `#${encodeURIComponent('#' + link)}`;
} else {
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_profile from './tf-profile.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_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';

View File

@ -16,7 +16,9 @@ class TfElement extends LitElement {
following: {type: Array},
users: {type: Object},
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.users = {};
this.loaded = false;
this.tags = [];
this.channels = [];
this.channels_unread = {};
this.channels_latest = {};
tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || [];
});
@ -64,6 +68,68 @@ class TfElement extends LitElement {
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
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) {
@ -72,8 +138,6 @@ class TfElement extends LitElement {
this.tab = 'search';
} else if (this.hash === '#connections') {
this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else {
@ -167,6 +231,14 @@ class TfElement extends LitElement {
`,
[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) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
@ -195,33 +267,37 @@ class TfElement extends LitElement {
}
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(
`
WITH
recent AS (SELECT id, json(content) AS content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
FROM recent
WHERE json_extract(content, '$.channel') IS NOT NULL),
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
FROM recent, json_each(recent.content, '$.mentions') AS mention
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
async get_latest_private(following) {
let latest = (await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages'))[0].latest;
const k_chunk_count = 256;
while (latest - k_chunk_count >= 0) {
let messages = 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.rowid > ?2 AND
messages.rowid <= ?3 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[new Date() - 7 * 24 * 60 * 60 * 1000]
);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
[
JSON.stringify(following),
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() {
let whoami = this.whoami;
let tags = this.load_recent_tags();
let following = await tfrpc.rpc.following([whoami], 3);
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
@ -233,7 +309,31 @@ class TfElement extends LitElement {
};
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();
users = await this.fetch_about(Object.keys(following).sort(), users);
console.log(
@ -243,14 +343,54 @@ class TfElement extends LitElement {
Object.keys(users).length,
'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.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`);
this.whoami = 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() {
let following = this.following;
let users = this.users;
@ -265,6 +405,10 @@ class TfElement extends LitElement {
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
?loading=${this.loading}
.channels=${this.channels}
.channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@ -275,14 +419,6 @@ class TfElement extends LitElement {
.broadcasts=${this.broadcasts}
></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') {
return html`
<tf-tab-search
@ -314,8 +450,6 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
}
@ -338,13 +472,15 @@ class TfElement extends LitElement {
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'@': 'mentions',
'🔍': 'search',
'👩‍💻': 'query',
};
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
class="w3-bar-item w3-button w3-circle w3-ripple"
@click=${this.refresh}
@ -384,13 +520,7 @@ class TfElement extends LitElement {
style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark"
>
${tabs}
<div style="padding: 8px">
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
</div>
${tabs} ${contents}
</div>
`;
}

View File

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

View File

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

View File

@ -11,6 +11,8 @@ class TfNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
};
}
@ -25,6 +27,7 @@ class TfNewsElement extends LitElement {
this.following = [];
this.drafts = {};
this.expanded = {};
this.channel_unread = -1;
}
process_messages(messages) {
@ -33,12 +36,13 @@ class TfNewsElement extends LitElement {
console.log('processing', messages.length, 'messages');
function ensure_message(id) {
function ensure_message(id, rowid) {
let found = messages_by_id[id];
if (found) {
return found;
} else {
let added = {
rowid: rowid,
id: id,
placeholder: true,
content: '"placeholder"',
@ -53,7 +57,7 @@ class TfNewsElement extends LitElement {
function link_message(message) {
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) {
parent.votes = [];
}
@ -62,14 +66,14 @@ class TfNewsElement extends LitElement {
} else if (message.content.type == 'post') {
if (message.content.root) {
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) {
m.child_messages = [];
}
m.child_messages.push(message);
message.parent_message = message.content.root;
} else {
let m = ensure_message(message.content.root[0]);
let m = ensure_message(message.content.root[0], message.rowid);
if (!m.child_messages) {
m.child_messages = [];
}
@ -162,6 +166,7 @@ class TfNewsElement extends LitElement {
} else {
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
@ -170,6 +175,13 @@ class TfNewsElement extends LitElement {
result.push(message);
}
}
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
}
return result;
}
@ -178,18 +190,39 @@ class TfNewsElement extends LitElement {
let final_messages = this.group_following(
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`
<div style="display: flex; flex-direction: column">
<div>
${final_messages.map(
(x) =>
html`<tf-message
html`
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
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>
`;

View File

@ -286,29 +286,29 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
`;
// prettier-ignore
const w3_2016_riverside = css`
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
const w3_2016_snorkel_blue = css`
.w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
.w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
.w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
.w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
.w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
.w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
.w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
.w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
.w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
.w3-theme-light {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-dark {color:#fff !important; background-color:#012641 !important}
.w3-theme-action {color:#fff !important; background-color:#012641 !important}
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
.w3-text-theme {color:#4c6a92 !important}
.w3-border-theme {border-color:#4c6a92 !important}
.w3-theme {color:#fff !important; background-color:#034f84 !important}
.w3-text-theme {color:#034f84 !important}
.w3-border-theme {border-color:#034f84 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
.w3-hover-text-theme:hover {color:#4c6a92 !important}
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important}
.w3-hover-text-theme:hover {color:#034f84 !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},
drafts: {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.drafts = {};
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() {
if (this.hash.startsWith('#@')) {
let r = await tfrpc.rpc.query(
channel() {
return this.hash.startsWith('##')
? 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
WHERE messages.author = ?
ORDER BY sequence DESC
LIMIT 20)
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
ORDER BY sequence DESC)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
UNION
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('#%')) {
return await tfrpc.rpc.query(
result = await tfrpc.rpc.query(
`
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
@ -62,6 +104,68 @@ class TfTabNewsFeedElement extends LitElement {
`,
[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 {
let promises = [];
const k_following_limit = 256;
@ -69,17 +173,17 @@ class TfTabNewsFeedElement extends LitElement {
promises.push(
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
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)
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
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
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
@ -88,50 +192,58 @@ class TfTabNewsFeedElement extends LitElement {
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
this.start_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,
start_time,
end_time,
]
)
);
}
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() {
this.loading++;
this.loading_canceled = false;
try {
let more = [];
while (!more.length && !this.loading_canceled) {
let last_start_time = this.start_time;
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
let more = await 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
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
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.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(this.start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) =>
x.timestamp >= this.start_time && x.timestamp < last_start_time
)
);
}
this.messages = await this.decrypt([...more, ...this.messages]);
} finally {
this.loading--;
}
}
cancel_load() {
this.loading_canceled = true;
}
async decrypt(messages) {
console.log('decrypt');
let result = [];
for (let message of messages) {
let content;
@ -156,8 +268,89 @@ class TfTabNewsFeedElement extends LitElement {
return result;
}
async add_messages(messages) {
this.messages = await this.decrypt([...messages, ...this.messages]);
async load_latest() {
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() {
@ -169,31 +362,49 @@ class TfTabNewsFeedElement extends LitElement {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
let self = this;
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));
});
this.load_messages();
}
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
if (!this.hash.startsWith('#%')) {
more = html`
<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
</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>
`;
}
return html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<tf-news
id="news"
whoami=${this.whoami}
@ -202,6 +413,8 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news>
${more}
`;

View File

@ -13,6 +13,9 @@ class TfTabNewsElement extends LitElement {
drafts: {type: Object},
expanded: {type: Object},
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.drafts = {};
this.expanded = {};
this.channels_unread = {};
this.channels_latest = {};
this.channels = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
@ -48,10 +54,7 @@ class TfTabNewsElement extends LitElement {
let unread = this.unread;
let news = this.shadowRoot?.getElementById('news');
if (news) {
console.log('injecting messages', news.messages);
news.add_messages(
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
);
news.load_latest();
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() {
let profile = this.hash.startsWith('#@')
let profile =
this.hash.startsWith('#@') && this.hash != '#@'
? html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)}
@ -129,13 +174,88 @@ class TfTabNewsElement extends LitElement {
</div>`;
}
return html`
<p class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${this.show_more}
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
style="width: 2in; left: 0; z-index: 5"
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()}
</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>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
@ -148,6 +268,7 @@ class TfTabNewsElement extends LitElement {
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose>
</div>
${profile}
@ -161,7 +282,10 @@ class TfTabNewsElement extends LitElement {
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed>
</div>
`;
}
}

View File

@ -18,7 +18,7 @@ class TfTagElement extends LitElement {
render() {
let number = this.count ? html` (${this.count})` : undefined;
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"
>${this.tag}${number}</a
>`;

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"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];
};
imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest('set_global_setting');
print('Setting', key, value);
let settings = await loadSettings();
settings[key] = value;

View File

@ -21,14 +21,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.24";
version = "0.0.25";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-XlmRr08UmScY//qxUEXHzagXHCFqARRYr3q8RK/jKFY=";
hash = "sha256-Rfk+CUhi+Ss0z70CCgmtVM/w4nCL1GX/MsD4sPYIa5s=";
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": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
@ -104,9 +104,9 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz",
"integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==",
"version": "6.10.6",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.6.tgz",
"integrity": "sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@ -118,20 +118,20 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz",
"integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==",
"version": "6.8.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.7",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.7.tgz",
"integrity": "sha512-6+iLsXvITWKHYlkgHPCs/qiX4dNzn8N78YfhOFvPtPYCkuXqZq10rAfsUMhOq7O/1VjJqdXRflyExlfVcu/9VQ==",
"version": "6.5.8",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz",
"integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@ -140,10 +140,13 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==",
"license": "MIT"
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2",
@ -158,12 +161,12 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.34.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.3.tgz",
"integrity": "sha512-Ph5d+u8DxIeSgssXEakaakImkzBV4+slwIbcxl9oc9evexJhImeu/G8TK7+zp+IFK9KuJ0BdSn6kTBJeH2CHvA==",
"version": "6.35.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.35.3.tgz",
"integrity": "sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.4.0",
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
@ -270,9 +273,9 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.19",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@ -300,6 +303,12 @@
"@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": {
"version": "15.3.0",
"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": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz",
"integrity": "sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
"integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [
"arm"
],
@ -383,9 +392,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.3.tgz",
"integrity": "sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
"integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [
"arm64"
],
@ -396,9 +405,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.3.tgz",
"integrity": "sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
"integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [
"arm64"
],
@ -409,9 +418,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.3.tgz",
"integrity": "sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
"integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [
"x64"
],
@ -422,9 +431,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.3.tgz",
"integrity": "sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
"integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
"cpu": [
"arm64"
],
@ -435,9 +444,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.3.tgz",
"integrity": "sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
"integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
"cpu": [
"x64"
],
@ -448,9 +457,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.3.tgz",
"integrity": "sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
"integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [
"arm"
],
@ -461,9 +470,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.3.tgz",
"integrity": "sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
"integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [
"arm"
],
@ -474,9 +483,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.3.tgz",
"integrity": "sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
"integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [
"arm64"
],
@ -487,9 +496,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.3.tgz",
"integrity": "sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
"integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [
"arm64"
],
@ -499,10 +508,23 @@
"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": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.3.tgz",
"integrity": "sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
"integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [
"ppc64"
],
@ -513,9 +535,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.3.tgz",
"integrity": "sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
"integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [
"riscv64"
],
@ -526,9 +548,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.3.tgz",
"integrity": "sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
"integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [
"s390x"
],
@ -539,9 +561,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz",
"integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
"integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [
"x64"
],
@ -552,9 +574,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.3.tgz",
"integrity": "sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
"integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [
"x64"
],
@ -565,9 +587,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.3.tgz",
"integrity": "sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
"integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [
"arm64"
],
@ -578,9 +600,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.3.tgz",
"integrity": "sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
"integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [
"ia32"
],
@ -591,9 +613,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.3.tgz",
"integrity": "sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
"integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [
"x64"
],
@ -780,9 +802,9 @@
}
},
"node_modules/rollup": {
"version": "4.27.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.3.tgz",
"integrity": "sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
"integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.6"
@ -795,24 +817,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.27.3",
"@rollup/rollup-android-arm64": "4.27.3",
"@rollup/rollup-darwin-arm64": "4.27.3",
"@rollup/rollup-darwin-x64": "4.27.3",
"@rollup/rollup-freebsd-arm64": "4.27.3",
"@rollup/rollup-freebsd-x64": "4.27.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.27.3",
"@rollup/rollup-linux-arm-musleabihf": "4.27.3",
"@rollup/rollup-linux-arm64-gnu": "4.27.3",
"@rollup/rollup-linux-arm64-musl": "4.27.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.27.3",
"@rollup/rollup-linux-riscv64-gnu": "4.27.3",
"@rollup/rollup-linux-s390x-gnu": "4.27.3",
"@rollup/rollup-linux-x64-gnu": "4.27.3",
"@rollup/rollup-linux-x64-musl": "4.27.3",
"@rollup/rollup-win32-arm64-msvc": "4.27.3",
"@rollup/rollup-win32-ia32-msvc": "4.27.3",
"@rollup/rollup-win32-x64-msvc": "4.27.3",
"@rollup/rollup-android-arm-eabi": "4.28.1",
"@rollup/rollup-android-arm64": "4.28.1",
"@rollup/rollup-darwin-arm64": "4.28.1",
"@rollup/rollup-darwin-x64": "4.28.1",
"@rollup/rollup-freebsd-arm64": "4.28.1",
"@rollup/rollup-freebsd-x64": "4.28.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
"@rollup/rollup-linux-arm-musleabihf": "4.28.1",
"@rollup/rollup-linux-arm64-gnu": "4.28.1",
"@rollup/rollup-linux-arm64-musl": "4.28.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
"@rollup/rollup-linux-riscv64-gnu": "4.28.1",
"@rollup/rollup-linux-s390x-gnu": "4.28.1",
"@rollup/rollup-linux-x64-gnu": "4.28.1",
"@rollup/rollup-linux-x64-musl": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2"
}
},
@ -894,9 +917,9 @@
}
},
"node_modules/terser": {
"version": "5.36.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"dev": true,
"license": "BSD-2-Clause",
"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;
unsigned int i;
#if defined(SQLITE3_H) || defined(SQLITE_STATIC_PERCENTILE)
(void)pApi; /* Unused parameter */
#else
#ifdef SQLITE3EXT_H
SQLITE_EXTENSION_INIT2(pApi);
#else
(void)pApi; /* Unused parameter */
#endif
(void)pzErrMsg; /* Unused parameter */
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
** 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
** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements
@ -18,7 +18,7 @@
** separate file. This file contains only code for the core SQLite library.
**
** The content in this amalgamation comes from Fossil check-in
** b95d11e958643b969c47a8e5857f3793b9e6.
** 2aabe05e2e8cae4847a802ee2daddc1d7413.
*/
#define SQLITE_CORE 1
#define SQLITE_AMALGAMATION 1
@ -462,9 +462,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.47.1"
#define SQLITE_VERSION_NUMBER 3047001
#define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e"
#define SQLITE_VERSION "3.47.2"
#define SQLITE_VERSION_NUMBER 3047002
#define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c"
/*
** 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 nDigit = 0; /* Number of digits processed */
int eType = 1; /* 1: pure integer, 2+: fractional -1 or less: bad UTF16 */
u64 s2; /* round-tripped significand */
double rr[2];
u64 s2;
assert( enc==SQLITE_UTF8 || enc==SQLITE_UTF16LE || enc==SQLITE_UTF16BE );
*pResult = 0.0; /* Default return value, in case of an error */
@ -35801,7 +35801,7 @@ do_atof_calc:
e = (e*esign) + d;
/* 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;
e--;
}
@ -35811,11 +35811,16 @@ do_atof_calc:
}
rr[0] = (double)s;
assert( sizeof(s2)==sizeof(rr[0]) );
memcpy(&s2, &rr[0], sizeof(s2));
if( s2<=0x43efffffffffffffLL ){
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);
}else{
rr[1] = 0.0;
}
assert( rr[1]<=1.0e-10*rr[0] ); /* Equal only when rr[0]==0.0 */
if( e>0 ){
while( e>=100 ){
e -= 100;
@ -147605,32 +147610,32 @@ static Expr *substExpr(
if( pSubst->isOuterJoin ){
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) ){
sqlite3SetJoinExpr(pNew, pExpr->w.iJoin,
pExpr->flags & (EP_OuterON|EP_InnerON));
}
sqlite3ExprDelete(db, pExpr);
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{
@ -254938,7 +254943,7 @@ static void fts5SourceIdFunc(
){
assert( nArg==0 );
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()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.47.1"
#define SQLITE_VERSION_NUMBER 3047001
#define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e"
#define SQLITE_VERSION "3.47.2"
#define SQLITE_VERSION_NUMBER 3047002
#define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c"
/*
** CAPI3REF: Run-Time Library Version Numbers

6
package-lock.json generated
View File

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

View File

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

View File

@ -385,8 +385,11 @@ public class TildeFriendsActivity extends Activity {
public void onServiceConnected(ComponentName name, IBinder binder) {
Log.w("tildefriends", "onServiceConnected");
Parcel data = Parcel.obtain();
ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(pipe_fd);
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(pipe_fd)) {
data.writeParcelable(pfd, 0);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e);
}
try {
binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY);
} 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");
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;
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");
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;
char* app_path = tf_malloc(path_length);

View File

@ -12,6 +12,8 @@ typedef struct _tf_packetstream_t
{
tf_packetstream_onreceive_t* onreceive;
void* onreceive_user_data;
tf_packetstream_on_close_t* on_close;
void* on_close_user_data;
uv_pipe_t stream;
char* buffer;
size_t buffer_size;
@ -30,6 +32,8 @@ void tf_packetstream_destroy(tf_packetstream_t* stream)
{
stream->onreceive = NULL;
stream->onreceive_user_data = NULL;
stream->on_close = NULL;
stream->on_close_user_data = NULL;
stream->destroyed = true;
if (stream->buffer)
{
@ -110,6 +114,14 @@ static void _packetstream_on_read(uv_stream_t* handle, ssize_t count, const uv_b
}
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_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);
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);
}
}
@ -162,6 +174,12 @@ void tf_packetstream_set_on_receive(tf_packetstream_t* stream, tf_packetstream_o
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)
{
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);
/**
** 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.
** @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);
/**
** 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.
** @param stream The packet stream.

View File

@ -294,6 +294,7 @@ typedef struct _tf_ssb_connection_t
uv_tcp_t tcp;
uv_connect_t connect;
uv_async_t async;
uv_async_t scheduled_async;
uv_timer_t handshake_timer;
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_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_connection_dispatch_scheduled(tf_ssb_connection_t* connection);
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)
{
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];
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,
.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)
@ -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);
}
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))
{
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);
}
if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->tcp.data && !connection->connect.data && !connection->handshake_timer.data &&
connection->ref_count == 0)
if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->scheduled_async.data && !connection->tcp.data && !connection->connect.data &&
!connection->handshake_timer.data && connection->ref_count == 0)
{
tf_free(connection->message_requests);
connection->message_requests = NULL;
@ -2792,6 +2804,8 @@ static tf_ssb_connection_t* _tf_ssb_connection_create(
connection->port = ntohs(addr->sin_port);
connection->async.data = connection;
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_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* connection = tf_ssb_connection_get(ssb, portal_id);
if (!connection)
{
return NULL;
}
JSContext* context = ssb->context;
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->async.data = tunnel;
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;
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->async.data = connection;
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);
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;
int old_pressure = connection->read_back_pressure;
connection->read_back_pressure += delta;
uv_async_send(&connection->scheduled_async);
if (!connection->closing)
{
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)
{
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)
@ -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);
JS_FreeValue(context, message);
tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id, connect_flags);
return true;
return tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id, connect_flags) != NULL;
}
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_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_size_by_author_index ON messages (author, length(content))");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_author_channel_index ON messages (content ->> 'type', author, content ->> 'channel');");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs ("
" 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,
"CREATE VIEW IF NOT EXISTS blob_wants_view (id, timestamp) AS "
" WITH wanted AS ( "
" SELECT messages_refs.ref AS id, messages.timestamp AS timestamp "
" FROM messages_refs "
" 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 "
" AND LENGTH(messages_refs.ref) = 52 "
" AND messages_refs.ref LIKE '&%.sha256'");
" AND LENGTH(wanted.id) = 52 "
" AND wanted.id LIKE '&%.sha256'");
bool need_add_flags = true;
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;
}
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;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3* reader = db ? db : tf_ssb_acquire_db_reader(ssb);
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 "
"permission.value = ?",
-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);
}
tf_ssb_release_db_reader(ssb, db);
if (reader != db)
{
tf_ssb_release_db_reader(ssb, reader);
}
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.
** @param ssb The SSB instance.
** @param db Optional database instance. If NULL, one will be acquired from ssb.
** @param id The user ID.
** @param permission The name of the 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.

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)
{
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);
if (tf_ssb_db_user_has_permission(ssb, db, work->user, "administration"))
{
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
@ -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));
}
tf_ssb_release_db_writer(ssb, db);
}
else
{
work->error = tf_strdup("not administrator");
}
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -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)
{
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] = "";
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;
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));
}
@ -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);
}
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));
}
@ -742,7 +742,7 @@ static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
{
identity_info_work_t* request = user_data;
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)))
{
@ -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;
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));
}
@ -1974,7 +1974,7 @@ enum
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)
{
@ -2003,6 +2003,21 @@ static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, co
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
{
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 };
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);
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 };
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);
if (found)
@ -2341,7 +2356,7 @@ typedef struct _following_t
static void _tf_ssb_following_work(tf_ssb_t* ssb, void* 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)

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;
if (sqlite3_prepare(db,
"SELECT previous, author, id, sequence, timestamp, hash, json(content), signature, flags FROM messages WHERE author = ?1 AND sequence > ?2 AND "
"sequence "
"< ?3 ORDER BY sequence",
"sequence < ?3 ORDER BY sequence",
-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 &&
@ -828,7 +827,8 @@ static void _tf_ssb_connection_send_history_stream_work(tf_ssb_connection_t* con
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
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;
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);
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. */
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++)
{
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);
}
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)
{
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. */
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);
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
{

View File

@ -16,6 +16,8 @@
#include <time.h>
#include <unistd.h>
#include "sodium/crypto_sign.h"
#if !defined(_WIN32)
#include <sys/wait.h>
#endif
@ -900,8 +902,7 @@ static void _write_file(const char* path, const char* contents)
FILE* file = fopen(path, "w");
if (!file)
{
printf("Unable to write %s: %s.\n", path, strerror(errno));
fflush(stdout);
tf_printf("Unable to write %s: %s.\n", path, strerror(errno));
abort();
}
fputs(contents, file);
@ -931,7 +932,7 @@ void tf_ssb_test_encrypt(const tf_test_options_t* options)
int result = system(command);
(void)result;
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
}
@ -1026,17 +1027,198 @@ void tf_ssb_test_publish(const tf_test_options_t* options)
int result = system(command);
(void)result;
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
tf_printf("returned %d\n", WEXITSTATUS(result));
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);
result = system(command);
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
uv_run(&loop, UV_RUN_DEFAULT);
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

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

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 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_cleanup(tf_taskstub_t* stub);
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);
}
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)
{
tf_task_t* parent = tf_task_get(context);
tf_taskstub_t* stub = tf_malloc(sizeof(tf_taskstub_t));
memset(stub, 0, sizeof(*stub));
stub->_stream = tf_packetstream_create();
tf_packetstream_set_on_close(stub->_stream, _tf_taskstub_packetstream_close, stub);
JSValue taskObject = JS_NewObjectClass(context, _classId);
JS_SetOpaque(taskObject, stub);
@ -314,35 +363,10 @@ static void _taskstub_finalizer(JSRuntime* runtime, JSValue value)
_taskstub_cleanup(stub);
}
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 _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 termination_signal)
{
tf_taskstub_t* stub = process->data;
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, 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);
_tf_taskstub_on_exit(stub, status, termination_signal);
}
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");
if (!file)
{
printf("Unable to write %s: %s.\n", path, strerror(errno));
fflush(stdout);
tf_printf("Unable to write %s: %s.\n", path, strerror(errno));
abort();
}
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, "peer_exchange", tf_ssb_test_peer_exchange, 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");
#endif
}

View File

@ -401,6 +401,11 @@ void tf_util_register(JSContext* context)
int tf_util_get_length(JSContext* context, JSValue value)
{
if (JS_IsUndefined(value))
{
return 0;
}
JSValue length = JS_GetPropertyStr(context, value, "length");
int result = 0;
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."

View File

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

View File

@ -34,6 +34,11 @@ def get_entries():
if m:
results.append((time.mktime(entry.get('updated_parsed')), name, entry.link, f'new issue #{m.group(1)}: {m.group(2)}'))
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='):
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]))

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)