Compare commits

...

102 Commits
main ... main

Author SHA1 Message Date
e0444510f4 trace: Use our own sequential thread ids to be more compatible with different trace viewers. 2025-01-24 19:00:40 -05:00
82e876a892 ssb: Only re-populate the messages_stats table on first creation. 2025-01-24 18:06:31 -05:00
c728e05032 ssb: Add a lookup table of max sequence and timestamp per account.f 2025-01-22 19:19:50 -05:00
c7a8ce7060 apps: display: grid. 2025-01-22 19:13:15 -05:00
762b4339fe ssb: Better 'Load More', too. 2025-01-22 18:28:55 -05:00
f824b8988e ssb: Correctness around tunnel.endpoints. 2025-01-22 18:03:05 -05:00
6280d6d167 apps: Mobile CSS tweaks. 2025-01-21 21:46:48 -05:00
4f18e744b4 ssb: Unbreak the channel query. 2025-01-21 21:31:16 -05:00
01d8f720e8 ssb: Treat timestamps of related messages separately from those of the primary queried messages. 2025-01-21 21:07:37 -05:00
18cf058af3 ssb: Experimenting with the news query strategy. 2025-01-21 20:53:23 -05:00
e2406df367 apps: Messing with CSS. 2025-01-20 20:45:35 -05:00
1fd669bdb3 update: npm. 2025-01-20 20:25:31 -05:00
f6add12c80 build: Update the change notes. 2025-01-20 17:57:15 -05:00
0f643bfe39 ssb: Complete using an invite, following the pub and posting a pub message. #99 2025-01-20 17:47:30 -05:00
15be498e4b ssb: Don't show connections in various states of disconnectedness in the sidebar. 2025-01-20 16:57:22 -05:00
fba465dd62 ssb: Allow a concurrent connection if the other one is a connection in the process of closing. 2025-01-20 16:49:21 -05:00
19dbe354e7 core: Consolidate default global setting values in one place. 2025-01-20 14:23:41 -05:00
fca5d37b7e ssb: Add an option to control whether we talk to strangers. #98 2025-01-20 13:35:28 -05:00
aa04ad2dc2 ssb: Clean up used and expired invites. 2025-01-20 08:01:24 -05:00
7ef4d814ef ssb: Avoid showing a broken img when viewing a profile without one assigned. 2025-01-19 21:34:42 -05:00
3f3deb665c ssb: Add a command-line action to generate an invite, and verified that Patchwork can accept it. 2025-01-19 21:00:38 -05:00
97fc22ce57 ssb: Test the one message generated from an invite so far. 2025-01-19 18:38:26 -05:00
616f3ad76d ssb: Invite support progress. Now the pub accepts the invite, tracks its use, and follows. The client still needs to react. 2025-01-19 17:02:08 -05:00
faca63946c build: Fix make docs. 2025-01-19 16:03:21 -05:00
57bae341a2 build: Missed an include. 2025-01-19 16:02:51 -05:00
fd09a766d2 ssb: Work in progress invite support. We can generate them. We can connect using an invite code. We can't yet invite.use(). 2025-01-19 16:00:37 -05:00
11564a5292 core: Let's try some neutral colors to avoid clashing with app colors. 2025-01-18 21:56:43 -05:00
ac12e350bf ssb: People without profile images get emoji faces. I'm amazed it took me this long. 2025-01-18 21:17:07 -05:00
2def15337d update: speedscope 1.22.0. 2025-01-17 17:55:47 -05:00
3e3d58a4a9 ssb: Try harder to avoid doing things with new connections during shutdown. 2025-01-16 12:47:50 -05:00
5ce4f55228 update: speedscope 1.21.2. 2025-01-16 12:35:43 -05:00
21788fc7b0 ssb: Stared at the news feed queries for performance and correctness a bit. 2025-01-15 19:32:31 -05:00
a1c4382fde update: libuv 1.50.0. 2025-01-15 18:53:34 -05:00
364e95698e ssb: Remove the react confirmation dialog. 2025-01-15 12:34:02 -05:00
6eec142499 update: sqlite 3.48.0. 2025-01-15 12:14:46 -05:00
a8f6b3a39a ssb: pub messaged needed padding. 2025-01-14 21:50:38 -05:00
250933bf41 ssb: Suppress noisy output when running command-line actions with output intended to be parsed. 2025-01-14 21:37:11 -05:00
56c77c781a ssb: Placeholder messages needed padding. 2025-01-14 21:18:49 -05:00
f7602b39a1 ssb: The compose preview needed some padding. 2025-01-14 20:51:56 -05:00
8c86092356 ssb: Make the refresh icon a circle. 2025-01-14 20:29:12 -05:00
db0a4bff77 ssb: Use most recent post timestamps to feature more relevant people to follow. 2025-01-14 20:15:18 -05:00
e198ff9cb1 ssb: Show some suggested accounts to follow. 2025-01-12 14:54:09 -05:00
b8eaa5cf97 ssb: prettier. 2025-01-12 12:08:50 -05:00
0d597721bf ssb: Try harder to avoid a full re-render. It's disruptive. 2025-01-12 12:01:52 -05:00
003e0caada ssb: Distant/unknown users get a circle avatar, too. 2025-01-12 11:56:59 -05:00
053637cfb4 ssb: The sync button indicates if a one-shot sync is active. 2025-01-12 11:54:30 -05:00
8178213f1a ssb: Keep the previous db location for android. wip release notes. 2025-01-11 16:14:39 -05:00
b4222a41de update: CodeMirror. 2025-01-11 16:02:52 -05:00
f28e409ea5 ssb: Add a get_contacts command to enumerate follows, blocks, and friends. 2025-01-11 15:49:49 -05:00
287c6c06e1 core: More clean shutdown? 2025-01-11 14:48:12 -05:00
8216bdb4b3 core: Poking at task cleanup. 2025-01-11 14:41:46 -05:00
aa15da50ab ssb: Tried to do the right lit things to prevent unnecessary re-rendering. Ended up doing a lazy JSON thing. 2025-01-11 14:09:42 -05:00
02759c6f83 ssb: Hint at follow depth with profile image shape. Also, reload follow information the same way we re-determine channel unread status. Let's see if this feels good. 2025-01-11 13:48:06 -05:00
6b0c49752c ssb: Trying to learn about occasional errors updating connection info in the database. 2025-01-11 13:02:30 -05:00
2e4f792fc3 ssb: Try to consolidate the local users list. Man, CSS. 2025-01-11 12:48:44 -05:00
17eba059f0 ssb: Fix trying to connect to the same stored connection over and over. 2025-01-11 12:32:18 -05:00
e59a00922b ssb: Respond to tunnel.endpoints. Patchwork didn't seem to like that we responded to tunnel.isRoom but not this. 2025-01-11 09:23:12 -05:00
872201c886 ssb: Support publishing private messages from the command-line. #89 2025-01-08 20:16:17 -05:00
3352098284 ssb: Connections established for a one-shot sync timeout due to inactivity so that the sync eventually completes. 2025-01-07 12:48:21 -05:00
d0bbd7f24f ssb: Add a has_blob command. #89 2025-01-06 20:46:16 -05:00
7f87714b58 ssb: Add some missing padding. 2025-01-05 21:43:26 -05:00
5594bee618 ssb: Add get_identity, get_sequence, and get_profile commands. #89 2025-01-05 17:16:05 -05:00
c469ef23e6 ssb: Make the profile layout a little friendlier. 2025-01-05 15:49:57 -05:00
f6e74f2526 ssb: Try to fix profile layout yet again. Man, CSS. 2025-01-05 15:41:56 -05:00
10b6e9c537 ssb: Remove the weird option to make the server account follow you. Now that this account is admin-controlled, it's unnecessary. 2025-01-05 15:29:29 -05:00
3f27af30b7 ssb: Actually fall back to this default global setting value. 2025-01-05 15:17:41 -05:00
23db09f9b7 core: Default to loading into the ssb app. No more messing around. 2025-01-05 14:52:27 -05:00
d1b7681efc ssb: Don't show 'Loading...' forever in the ssb app when not signed in. Direct to the login page. 2025-01-05 12:52:52 -05:00
61ad405ad8 ssb: Too bright. 2025-01-04 21:38:52 -05:00
aff98110e0 ssb: Tidy up some of the more common reasons for disconnect. 2025-01-04 21:37:43 -05:00
2f36db9142 ssb: Don't display mentions for tags that are used in the text of a message. 2025-01-04 21:05:24 -05:00
aa86ee1066 cleanup: rm test.c 2025-01-04 17:12:00 -05:00
dbbcce8165 ssb: Don't store connections that aren't user-initiated. 2025-01-04 17:08:36 -05:00
1ed066ef0f ssb: Fix naked hashtag links (to the corresponding channel). 2025-01-04 13:03:58 -05:00
763f7d45d8 ssb: Prettier. 2025-01-04 12:41:04 -05:00
2328f3afb5 ssb: Ease up on excessively re-hitting the database for ebt.replicate even more. 2025-01-04 09:58:16 -05:00
2223245861 ssb: Maybe ease up on hammering the db for follows. 2025-01-03 18:17:54 -05:00
36226b01cd ssb: Manage new message handling from the new EBT code. 2025-01-03 17:04:38 -05:00
da31f9cadd cleanup: get/set sent clock is now unused. 2025-01-03 16:56:04 -05:00
9da4857066 ssb: Make the client a bit less aggressive about determining private messages every load. 2025-01-03 15:53:19 -05:00
75c71135ba ssb: No longer replicate every account we hear about. 2025-01-03 15:25:59 -05:00
0cb5025a16 core: Improve global setting grammar. 2025-01-03 14:10:27 -05:00
44d9f69434 ssb: Refactoring EBT implementation. I think this works not worse than before and will let me schedule message replication better in a future change. #93 2025-01-03 13:59:25 -05:00
3f343b283b ssb: Delete one more redundant global setting accessor. 2025-01-03 08:41:13 -05:00
03a28fc3c5 update: CodeMirror. 2025-01-03 08:19:41 -05:00
3513619221 ssb: Reduce message margins. 2025-01-02 21:16:55 -05:00
0c9f5769d3 build: Trying to fix flatpak build for some reason. 2025-01-02 17:45:27 -05:00
587a666ab6 build: Needed out/ earlier for ssl-local. 2025-01-02 17:40:29 -05:00
f26deea508 build: mkdir out/openssl-local. 2025-01-02 17:35:49 -05:00
b8e19040b5 ssb: Fiddling with render of encrypted messages. 2025-01-02 16:11:04 -05:00
7d9e0f4080 macos: Fix build. 2025-01-02 14:20:58 -05:00
16ce7fbc7b ssb: Fix the message encrypted icon placement. 2025-01-02 13:55:47 -05:00
639fce376a ssb: More uv_async_send paranoia still. #96 2025-01-02 13:01:09 -05:00
3cdbac5c22 build: Archive the windows .exe with data. 2025-01-02 13:00:42 -05:00
3dcafdf403 ssb: More uv_async_send paranoia. #96 2025-01-02 12:40:11 -05:00
cd2fe9f8d9 ssb: Fix a crash on Windows when we would call uv_async_send on a handle that had already been closed. Various other cleanup and improvements along the journey. #96 2025-01-02 12:35:58 -05:00
fd40596ce7 ssb: Every now and then I load in Chrome and see everywhere I used overflow: scroll when I wanted overflow: auto. 2025-01-02 08:58:10 -05:00
7ecda69703 ssb: tags never got an updated CSS treatment. 2025-01-02 08:49:30 -05:00
a3b76cd5c2 core: Let's try getting crash callstacks on win32 with a vectored exception handler. 2025-01-02 08:32:18 -05:00
54df862998 ssb: Continuing to untangle message CSS. 2025-01-01 16:44:16 -05:00
301b7a4911 ssb: Trying to untangle some message formatting ugliness. First step: some minor refactoring. 2025-01-01 15:45:11 -05:00
e0a048abe6 follow: This app had never been updated since jsonb, whoops. #94 2025-01-01 15:28:19 -05:00
72 changed files with 5638 additions and 2831 deletions

View File

@ -31,7 +31,7 @@ jobs:
with: with:
path: | path: |
out/TildeFriends-release.fdroid.apk out/TildeFriends-release.fdroid.apk
out/winrelease/tildefriends.exe out/winrelease/tildefriends.standalone.exe
out/tildefriends-x86_64.AppImage out/tildefriends-x86_64.AppImage
out/release/tildefriends.standalone out/release/tildefriends.standalone
out/armrelease/tildefriends.standalone out/armrelease/tildefriends.standalone

View File

@ -20,7 +20,7 @@ VERSION_CODE := 32
VERSION_NUMBER := 0.0.27-wip VERSION_NUMBER := 0.0.27-wip
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3480000.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
@ -127,6 +127,7 @@ WINDOWS_TARGETS := \
out/winrelease/tildefriends.exe out/winrelease/tildefriends.exe
ifeq ($(HAVE_WIN),1) ifeq ($(HAVE_WIN),1)
BUILD_TYPES += windebug winrelease BUILD_TYPES += windebug winrelease
all: out/windebug/tildefriends.standalone.exe out/winrelease/tildefriends.standalone.exe
endif endif
AARCH64_TARGETS := \ AARCH64_TARGETS := \
@ -1091,6 +1092,7 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@cp -v $< $@ @cp -v $< $@
out/data.zip: $(RAW_FILES) out/data.zip: $(RAW_FILES)
@echo [zip] $@
@zip -u $@ -q -9 $(RAW_FILES) @zip -u $@ -q -9 $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "💻", "emoji": "💻",
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256" "previous": "&sFRTDn/RpxP1NJeECXHrXKwCRUJsEOEDVaCMPl50zpM=.sha256"
} }

View File

@ -19,10 +19,6 @@ async function fetch_info(apps) {
return result; return result;
} }
/**
*
*
*/
async function fetch_shared_apps() { async function fetch_shared_apps() {
let messages = {}; let messages = {};
@ -69,17 +65,17 @@ async function main() {
const stylesheet = ` const stylesheet = `
body { body {
color: whitesmoke; color: whitesmoke;
font-family: sans-serif; margin: 8px;
margin: 16px;
} }
.container {
.iconbox {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, 64px); grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 1em; }
justify-content: space-around;
background-color: #ffffff10; .iconbox::after {
border: 2px solid #073642; content: "";
border-radius: 8px; flex: auto;
} }
.app { .app {
@ -101,16 +97,28 @@ async function main() {
`; `;
const body = ` const body = `
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1> <h1>Welcome to Tilde Friends</h1>
<h2>your apps</h2> <div class="w3-card-4 w3-margin-top">
<div id="apps" class="container"></div> <header class="w3-container w3-light-blue">
<h2>Your Apps</h2>
</header>
<div id="apps" class="w3-indigo iconbox"></div>
</div>
<h2>shared apps</h2> <div class="w3-card-4 w3-margin-top">
<div id="shared_apps" class="container"></div> <header class="w3-container w3-light-blue">
<h2>Shared Apps</h2>
</header>
<div id="shared_apps" class="w3-indigo iconbox"></div>
</div>
<h2>core apps</h2> <div class="w3-card-4 w3-margin-top">
<div id="core_apps" class="container"></div> <header class="w3-container w3-light-blue">
<h2>Core Apps</h2>
</header>
<div id="core_apps" class="w3-indigo iconbox"></div>
</div>
`; `;
const script = ` const script = `
@ -126,9 +134,13 @@ async function main() {
// For each app in the provided list // For each app in the provided list
for (let app of Object.keys(apps).sort()) { for (let app of Object.keys(apps).sort()) {
// Create the item // Create the item
let div = list.appendChild(document.createElement('div')); let inline = document.createElement('div');
inline.style.display = 'inline-block';
inline.classList.add('w3-button');
list.appendChild(inline);
let div = document.createElement('div');
inline.appendChild(div);
div.classList.add('app'); div.classList.add('app');
// The app's icon // The app's icon
@ -161,12 +173,13 @@ async function main() {
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link type="text/css" rel="stylesheet" href="w3.css"></link>
<style> <style>
${stylesheet} ${stylesheet}
</style> </style>
</head> </head>
<body> <body class="w3-darkgray">
${body} ${body}
</body> </body>

235
apps/apps/w3.css Normal file
View File

@ -0,0 +1,235 @@
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

View File

@ -1,4 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "➡️" "emoji": "➡️",
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
} }

View File

@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
result.blocking = result.blocking || {}; result.blocking = result.blocking || {};
let contacts = await query( let contacts = await query(
` `
SELECT content FROM messages SELECT json(content) AS content FROM messages
WHERE author = ? AND WHERE author = ? AND
rowid > ? AND rowid > ? AND
rowid <= ? AND rowid <= ? AND
@ -189,50 +189,6 @@ async function fetch_about(db, ids, users) {
return Object.assign({}, users); return Object.assign({}, users);
} }
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
let o = await db.get(id + ':about');
const k_version = 4;
let f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlAsync(
'SELECT ' +
' sequence, ' +
' content ' +
'FROM messages ' +
'WHERE ' +
' author = ?1 AND ' +
' sequence > ?2 AND ' +
" json_extract(content, '$.type') = 'about' AND " +
" json_extract(content, '$.about') = ?1 " +
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
'ORDER BY sequence',
[id, f.sequence],
function (row) {
f.sequence = row.sequence;
if (row.content) {
let about = {};
try {
about = JSON.parse(row.content);
} catch {}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
}
);
let j = JSON.stringify(f);
if (o != j) {
await db.set(id + ':about', j);
}
g_about_cache[id] = f.about;
return f.about;
}
async function getSize(db, id) { async function getSize(db, id) {
let size = 0; let size = 0;
await ssb.sqlAsync( await ssb.sqlAsync(

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🦀",
"previous": "&bBoMW+AjErDfa483Mg3+h1L25xfDDeVSpcfD9WAwL3U=.sha256" "previous": "&dQu+yQ4/be0vcgIZSMKtesQCbH8lVhHcZAEv9+dNknY=.sha256"
} }

View File

@ -21,9 +21,6 @@ tfrpc.register(async function createIdentity() {
tfrpc.register(async function getServerIdentity() { tfrpc.register(async function getServerIdentity() {
return ssb.getServerIdentity(); return ssb.getServerIdentity();
}); });
tfrpc.register(async function setServerFollowingMe(id, following) {
return ssb.setServerFollowingMe(id, following);
});
tfrpc.register(async function getIdentities() { tfrpc.register(async function getIdentities() {
return ssb.getIdentities(); return ssb.getIdentities();
}); });
@ -106,6 +103,10 @@ tfrpc.register(async function getActiveIdentity() {
tfrpc.register(async function sync() { tfrpc.register(async function sync() {
return await ssb.sync(); return await ssb.sync();
}); });
tfrpc.register(async function url() {
return core.url;
});
core.register('onBroadcastsChanged', async function () { core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
}); });

View File

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

View File

@ -18,6 +18,8 @@ class TfElement extends LitElement {
channels: {type: Array}, channels: {type: Array},
channels_unread: {type: Object}, channels_unread: {type: Object},
channels_latest: {type: Object}, channels_latest: {type: Object},
guest: {type: Boolean},
url: {type: String},
}; };
} }
@ -36,8 +38,8 @@ class TfElement extends LitElement {
this.channels = []; this.channels = [];
this.channels_unread = {}; this.channels_unread = {};
this.channels_latest = {}; this.channels_latest = {};
this.loading_channels_latest = 0; this.loading_latest = 0;
this.loading_channels_latest_scheduled = 0; this.loading_latest_scheduled = 0;
tfrpc.rpc.getBroadcasts().then((b) => { tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || []; self.broadcasts = b || [];
}); });
@ -66,7 +68,9 @@ class TfElement extends LitElement {
async initial_load() { async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || []; let ids = (await tfrpc.rpc.getIdentities()) || [];
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
this.ids = ids; this.ids = ids;
await this.load_channels(); await this.load_channels();
} }
@ -143,7 +147,8 @@ class TfElement extends LitElement {
} }
} }
async fetch_about(ids, users) { async fetch_about(following, users) {
let ids = Object.keys(following).sort();
const k_cache_version = 1; const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about'); let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache; let original_cache = cache;
@ -158,8 +163,8 @@ class TfElement extends LitElement {
let max_row_id = ( let max_row_id = (
await tfrpc.rpc.query( await tfrpc.rpc.query(
` `
SELECT MAX(rowid) AS max_row_id FROM messages SELECT MAX(rowid) AS max_row_id FROM messages
`, `,
[] []
) )
)[0].max_row_id; )[0].max_row_id;
@ -172,7 +177,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query( let abouts = await tfrpc.rpc.query(
` `
SELECT SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature messages.author, json(messages.content) AS content, messages.sequence
FROM FROM
messages, messages,
json_each(?1) AS following json_each(?1) AS following
@ -183,7 +188,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about' json_extract(messages.content, '$.type') = 'about'
UNION UNION
SELECT SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature messages.author, json(messages.content) AS content, messages.sequence
FROM FROM
messages, messages,
json_each(?2) AS following json_each(?2) AS following
@ -221,7 +226,11 @@ class TfElement extends LitElement {
} }
users = users || {}; users = users || {};
for (let id of Object.keys(cache.about)) { for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(users[id] || {}, cache.about[id]); users[id] = Object.assign(
{follow_depth: following[id]?.d},
users[id] || {},
cache.about[id]
);
} }
return Object.assign({}, users); return Object.assign({}, users);
} }
@ -244,7 +253,7 @@ class TfElement extends LitElement {
this.load_channels(); this.load_channels();
} }
} }
this.schedule_load_channels_latest(); this.schedule_load_latest();
} }
async _handle_whoami_changed(event) { async _handle_whoami_changed(event) {
@ -259,144 +268,199 @@ class TfElement extends LitElement {
} }
} }
async create_identity() {
if (confirm('Are you sure you want to create a new identity?')) {
await tfrpc.rpc.createIdentity();
this.ids = (await tfrpc.rpc.getIdentities()) || [];
if (this.ids && !this.whoami) {
this.whoami = this.ids[0];
}
}
}
async get_latest_private(following) { async get_latest_private(following) {
const k_version = 1;
// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
let cache = JSON.parse(
(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
);
if (cache.version !== k_version) {
cache = {
version: k_version,
messages: [],
range: [],
};
}
let latest = ( let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages') await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest; )[0].latest;
const k_chunk_count = 256; let ranges = [];
while (latest - k_chunk_count >= 0) { const k_chunk_size = 512;
if (cache.range.length) {
for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
ranges.push([
Math.max(i - k_chunk_size, 0),
Math.min(cache.range[0], i + k_chunk_size),
false,
]);
}
} else {
for (let i = 0; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
}
for (let range of ranges) {
let messages = await tfrpc.rpc.query( let messages = await tfrpc.rpc.query(
` `
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT messages.rowid, messages.id, json(content) AS content
FROM messages FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE WHERE
messages.rowid > ?2 AND messages.rowid > ?1 AND
messages.rowid <= ?3 AND messages.rowid <= ?2 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY sequence DESC ORDER BY sequence DESC
`, `,
[JSON.stringify(following), latest - k_chunk_count, latest] [range[0], range[1]]
); );
messages = (await this.decrypt(messages)).filter((x) => x.decrypted); messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
if (messages.length) { if (messages.length) {
return Math.max(...messages.map((x) => x.rowid)); cache.latest = Math.max(
cache.latest ?? 0,
...messages.map((x) => x.rowid)
);
if (range[2]) {
cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
} else {
cache.messages = [...messages.map((x) => x.id), ...cache.messages];
}
} }
latest -= k_chunk_count; cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
await tfrpc.rpc.databaseSet(
`private:${this.whoami}`,
JSON.stringify(cache)
);
} }
return -1; return cache.latest;
} }
async load_channels_latest(following) { async load_channels_latest(following) {
this.loading_channels_latest++; let start_time = new Date();
try { let latest_private = this.get_latest_private(following);
let start_time = new Date(); let channels = await tfrpc.rpc.query(
let latest_private = this.get_latest_private(following); `
let channels = await 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
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages JOIN json_each(?2) AS following ON messages.author = following.value
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value WHERE
JOIN json_each(?2) AS following ON messages.author = following.value messages.content ->> 'type' = 'post' AND
WHERE messages.content ->> 'root' IS NULL AND
messages.content ->> 'type' = 'post' AND messages.author != ?4
messages.content ->> 'root' IS NULL AND GROUP by channel
messages.author != ?4 UNION
GROUP by channel SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
UNION JOIN json_each(?2) AS following ON messages.author = following.value
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages WHERE
JOIN json_each(?2) AS following ON messages.author = following.value messages.content ->> 'type' = 'post' AND
WHERE messages.content ->> 'root' IS NULL AND
messages.content ->> 'type' = 'post' AND messages.author != ?4
messages.content ->> 'root' IS NULL AND UNION
messages.author != ?4 SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
UNION JOIN messages ON messages.rowid = messages_fts.rowid
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3) JOIN json_each(?2) AS following ON messages.author = following.value
JOIN messages ON messages.rowid = messages_fts.rowid WHERE messages.author != ?4
JOIN json_each(?2) AS following ON messages.author = following.value `,
WHERE messages.author != ?4 [
`, JSON.stringify(this.channels),
[ JSON.stringify(following),
JSON.stringify(this.channels), '"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(following), this.whoami,
'"' + this.whoami.replace('"', '""') + '"', ]
this.whoami, );
] this.channels_latest = Object.fromEntries(
); channels.map((x) => [x.channel, x.rowid])
this.channels_latest = Object.fromEntries( );
channels.map((x) => [x.channel, x.rowid]) console.log('channels took', (new Date() - start_time) / 1000.0);
); let self = this;
console.log('latest', this.channels_latest); start_time = new Date();
console.log('unread', this.channels_unread); latest_private.then(function (latest) {
console.log('channels took', (new Date() - start_time) / 1000.0); self.channels_latest = Object.assign({}, self.channels_latest, {
let self = this; '🔐': latest,
latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest,
});
console.log('private took', (new Date() - start_time) / 1000.0);
}); });
} finally { console.log('private took', (new Date() - start_time) / 1000.0);
this.loading_channels_latest--; });
}
} }
_schedule_load_channels_latest_timer() { _schedule_load_latest_timer() {
--this.loading_channels_latest_scheduled; --this.loading_latest_scheduled;
this.schedule_load_channels_latest(); this.schedule_load_latest();
} }
schedule_load_channels_latest() { schedule_load_latest() {
if (!this.loading_channels_latest) { if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest(); this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load_channels_latest(this.following); this.load();
} else if (!this.loading_channels_latest_scheduled) { } else if (!this.loading_latest_scheduled) {
this.loading_channels_latest_scheduled++; this.loading_latest_scheduled++;
setTimeout(this._schedule_load_channels_latest_timer.bind(this), 5000); setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
} }
} }
async fetch_user_info(users) {
let info = await tfrpc.rpc.query(
`
SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
JOIN json_each(?) AS following
ON messages_stats.author = following.value
`,
[JSON.stringify(Object.keys(users))]
);
for (let row of info) {
users[row.author].seq = row.max_seq;
users[row.author].ts = row.max_ts;
}
return users;
}
async load() { async load() {
let start_time = new Date(); this.loading_latest = true;
let whoami = this.whoami; try {
let following = await tfrpc.rpc.following([whoami], 2); let start_time = new Date();
let users = {}; let whoami = this.whoami;
let by_count = []; let following = await tfrpc.rpc.following([whoami], 2);
for (let [id, v] of Object.entries(following)) { let users = {};
users[id] = { let by_count = [];
following: v.of, for (let [id, v] of Object.entries(following)) {
blocking: v.ob, users[id] = {
followed: v.if, following: v.of,
blocked: v.ib, blocking: v.ob,
}; followed: v.if,
by_count.push({count: v.of, id: id}); blocked: v.ib,
};
by_count.push({count: v.of, id: id});
}
this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
this.following = Object.keys(following);
users = await this.fetch_about(following, users);
console.log(
'about took',
(new Date() - start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
start_time = new Date();
users = await this.fetch_user_info(users);
console.log(
'user info took',
(new Date() - start_time) / 1000.0,
'seconds'
);
this.users = users;
console.log(
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
this.whoami = whoami;
this.loaded = whoami;
} finally {
this.loading_latest = false;
} }
this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
users = await this.fetch_about(Object.keys(following).sort(), users);
console.log(
'about took',
(new Date() - start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
this.following = Object.keys(following);
this.users = users;
console.log(`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`);
this.whoami = whoami;
this.loaded = whoami;
} }
channel_set_unread(event) { channel_set_unread(event) {
@ -521,7 +585,9 @@ class TfElement extends LitElement {
style="position: static; top: 0; z-index: 10" style="position: static; top: 0; z-index: 10"
> >
<button <button
class="w3-bar-item w3-button w3-circle w3-ripple" class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh} @click=${this.refresh}
> >
@ -544,8 +610,20 @@ class TfElement extends LitElement {
)} )}
</div> </div>
`; `;
let contents = let contents = this.guest
!this.loaded || this.loading ? html`<div
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
>
<p>🦀 Must be logged in to Tilde Friends to scuttle here. 🦀</p>
<footer class="w3-center">
<a
class="w3-button w3-theme-d1"
href=${`/login?return=${encodeURIComponent(this.url)}`}
>Login</a
>
</footer>
</div>`
: !this.loaded || this.loading
? html`<div ? html`<div
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge" class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
> >
@ -559,7 +637,7 @@ class TfElement extends LitElement {
class="w3-theme-dark" class="w3-theme-dark"
> >
<div style="flex: 0 0">${tabs}</div> <div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: scroll; contain: layout"> <div style="flex: 1 1; overflow: auto; contain: layout">
${contents} ${contents}
</div> </div>
</div> </div>

View File

@ -594,7 +594,7 @@ class TfComposeElement extends LitElement {
.innerText=${live(draft.text ?? '')} .innerText=${live(draft.text ?? '')}
></span> ></span>
</div> </div>
<div class="w3-half"> <div class="w3-half w3-container">
${content_warning} ${content_warning}
<p id="preview"></p> <p id="preview"></p>
</div> </div>
@ -605,7 +605,11 @@ class TfComposeElement extends LitElement {
<footer class="w3-container"> <footer class="w3-container">
${this.render_attach_app()} ${this.render_content_warning()} ${this.render_attach_app()} ${this.render_content_warning()}
${this.render_new_thread()} ${this.render_new_thread()}
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}> <button
class="w3-button w3-theme-d1"
id="submit"
@click=${this.submit}
>
Submit Submit
</button> </button>
<button class="w3-button w3-theme-d1" @click=${this.attach}> <button class="w3-button w3-theme-d1" @click=${this.attach}>

View File

@ -1,4 +1,4 @@
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js'; import {LitElement, html, repeat, render, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js'; import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js'; import * as emojis from './emojis.js';
@ -97,6 +97,13 @@ class TfMessageElement extends LitElement {
} }
} }
render_json(value) {
let json = JSON.stringify(value, null, 2);
return html`
<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
`;
}
render_raw() { render_raw() {
let raw = { let raw = {
id: this.message?.id, id: this.message?.id,
@ -108,36 +115,24 @@ class TfMessageElement extends LitElement {
content: this.message?.content, content: this.message?.content,
signature: this.message?.signature, signature: this.message?.signature,
}; };
return html`<div style="white-space: pre-wrap"> return this.render_json(raw);
${JSON.stringify(raw, null, 2)}
</div>`;
} }
vote(emoji) { vote(emoji) {
let reaction = emoji; let reaction = emoji;
let message = this.message.id; let message = this.message.id;
if ( tfrpc.rpc
confirm( .appendMessage(this.whoami, {
'Are you sure you want to react with ' + type: 'vote',
reaction + vote: {
' to ' + link: message,
message + value: 1,
'?' expression: reaction,
) },
) { })
tfrpc.rpc .catch(function (error) {
.appendMessage(this.whoami, { alert(error?.message);
type: 'vote', });
vote: {
link: message,
value: 1,
expression: reaction,
},
})
.catch(function (error) {
alert(error?.message);
});
}
} }
react(event) { react(event) {
@ -190,7 +185,7 @@ class TfMessageElement extends LitElement {
render_mention(mention) { render_mention(mention) {
if (!mention?.link || typeof mention.link != 'string') { if (!mention?.link || typeof mention.link != 'string') {
return html` <pre>${JSON.stringify(mention)}</pre>`; return this.render_json(mention);
} else if ( } else if (
mention?.link?.startsWith('&') && mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/') mention?.type?.startsWith('image/')
@ -241,16 +236,17 @@ class TfMessageElement extends LitElement {
) { ) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`; return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else { } else {
return html` <pre style="white-space: pre-wrap"> return this.render_json(mention);
${JSON.stringify(mention, null, 2)}</pre
>`;
} }
} }
render_mentions() { render_mentions() {
let mentions = this.message?.content?.mentions || []; let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter( mentions = mentions.filter(
(x) => this.message?.content?.text?.indexOf(x.link) === -1 (x) =>
this.message?.content?.text?.indexOf(
typeof x === 'string' ? x : x.link
) === -1
); );
if (mentions.length) { if (mentions.length) {
let self = this; let self = this;
@ -307,7 +303,9 @@ ${JSON.stringify(mention, null, 2)}</pre
@click=${() => self.set_expanded(false)} @click=${() => self.set_expanded(false)}
> >
Collapse</button Collapse</button
>${(this.message.child_messages || []).map( >${repeat(
this.message.child_messages || [],
(x) => x.id,
(x) => (x) =>
html`<tf-message html`<tf-message
.message=${x} .message=${x}
@ -357,31 +355,38 @@ ${JSON.stringify(mention, null, 2)}</pre
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`); return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
} }
render() { class_background() {
let content = this.message?.content; return this.message?.decrypted
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red' ? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread : this.message?.rowid >= this.channel_unread
? 'w3-theme-d2' ? 'w3-theme-d2'
: 'w3-theme-d4'; : 'w3-theme-d4';
let self = this; }
get_content() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
return content;
}
render_raw_button() {
let content = this.get_content();
let raw_button; let raw_button;
switch (this.format) { switch (this.format) {
case 'raw': case 'raw':
if (content?.type == 'post' || content?.type == 'blog') { if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'md')} @click=${() => (this.format = 'md')}
> >
Markdown Markdown
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')} @click=${() => (this.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -390,7 +395,7 @@ ${JSON.stringify(mention, null, 2)}</pre
case 'md': case 'md':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')} @click=${() => (this.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -398,7 +403,7 @@ ${JSON.stringify(mention, null, 2)}</pre
case 'decrypted': case 'decrypted':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')} @click=${() => (this.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
@ -407,58 +412,136 @@ ${JSON.stringify(mention, null, 2)}</pre
if (this.message.decrypted) { if (this.message.decrypted) {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'decrypted')} @click=${() => (this.format = 'decrypted')}
> >
Decrypted Decrypted
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')} @click=${() => (this.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
} }
break; break;
} }
function small_frame(inner) { return raw_button;
let body; }
return html`
<div render_header() {
class="w3-card-4 ${class_background} w3-border-theme" let is_encrypted = this.message?.decrypted
style="margin-top: 8px; padding: 16px; display: inline-block; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%" ? html`<span class="w3-bar-item">🔓</span>`
> : typeof this.message?.content == 'string'
<tf-user id=${self.message.author} .users=${self.users}></tf-user> ? html`<span class="w3-bar-item">🔒</span>`
<span style="padding-right: 8px; text-wrap: nowrap" : undefined;
><a tfarget="_top" href=${'#' + encodeURIComponent(self.message.id)} return html`
>%</a <header class="w3-bar">
> <span class="w3-bar-item">
${new Date(self.message.timestamp).toLocaleString()}</span <tf-user id=${this.message.author} .users=${this.users}></tf-user>
</span>
${is_encrypted}
<span class="w3-bar-item w3-right">${this.render_raw_button()}</span>
<span class="w3-bar-item w3-right" style="text-wrap: nowrap"
><a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>%</a
> >
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner} ${new Date(this.message.timestamp).toLocaleString()}</span
${self.render_votes()} >
${(self.message.child_messages || []).map( </header>
(x) => html` `;
<tf-message }
.message=${x}
whoami=${self.whoami} render_frame(inner) {
.users=${self.users} return html`
.drafts=${self.drafts} <style>
.expanded=${self.expanded} code {
channel=${self.channel} white-space: pre-wrap;
channel_unread=${self.channel_unread} overflow-wrap: break-word;
></tf-message> }
` div {
)} overflow-wrap: anywhere;
</div> }
`; img {
} max-width: 100%;
if (this.message?.type === 'contact_group') { height: auto;
return html` <div display: block;
class="w3-card-4 ${class_background} w3-border-theme" }
style="margin-top: 8px; padding: 16px; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%" </style>
<div
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%"
> >
${this.message.messages.map( ${inner}
</div>
`;
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
${self.render_header()}
${self.format == 'raw'
? html`<div class="w3-container">${self.render_raw()}</div>`
: inner}
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
></tf-message>
`
)}
`);
}
render_actions() {
let content = this.get_content();
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<div class="w3-section w3-container">
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</div>
`;
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.class_background();
let self = this;
if (this.message?.type === 'contact_group') {
return this.render_frame(
html` ${this.message.messages.map(
(x) => (x) =>
html`<tf-message html`<tf-message
.message=${x} .message=${x}
@ -469,33 +552,34 @@ ${JSON.stringify(mention, null, 2)}</pre
channel=${this.channel} channel=${this.channel}
channel_unread=${this.channel_unread} channel_unread=${this.channel_unread}
></tf-message>` ></tf-message>`
)} )}`
</div>`; );
} else if (this.message.placeholder) { } else if (this.message.placeholder) {
return html` <div return this.render_frame(
class="w3-card-4 ${class_background} w3-border-theme" html`<div class="w3-padding">
style="margin-top: 8px; padding: 16px; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%" <p>
> <a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
<a target="_top" href=${'#' + encodeURIComponent(this.message.id)} >${this.message.id}</a
>${this.message.id}</a >
> (placeholder)
(placeholder) </p>
<div>${this.render_votes()}</div> <div>${this.render_votes()}</div>
${(this.message.child_messages || []).map( ${(this.message.child_messages || []).map(
(x) => html` (x) => html`
<tf-message <tf-message
.message=${x} .message=${x}
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel} channel=${this.channel}
channel_unread=${this.channel_unread} channel_unread=${this.channel_unread}
></tf-message> ></tf-message>
` `
)} )}
</div>`; </div>`
} else if (typeof (content?.type === 'string')) { );
} else if (typeof content?.type === 'string') {
if (content.type == 'about') { if (content.type == 'about') {
let name; let name;
let image; let image;
@ -522,10 +606,14 @@ ${JSON.stringify(mention, null, 2)}</pre
Updated profile for Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>. <tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`; </div>`;
return small_frame(html` ${update} ${name} ${image} ${description} `); return this.render_small_frame(html`
<div class="w3-container">
<p>${update} ${name} ${image} ${description}</p>
</div>
`);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return html` return html`
<div> <div class="w3-padding">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
is is
${content.blocking === true ${content.blocking === true
@ -544,24 +632,6 @@ ${JSON.stringify(mention, null, 2)}</pre
</div> </div>
`; `;
} else if (content.type == 'post') { } else if (content.type == 'post') {
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
let self = this; let self = this;
let body; let body;
switch (this.format) { switch (this.format) {
@ -578,11 +648,7 @@ ${JSON.stringify(mention, null, 2)}</pre
body = unsafeHTML(tfutils.markdown(content.text)); body = unsafeHTML(tfutils.markdown(content.text));
break; break;
case 'decrypted': case 'decrypted':
body = html`<pre body = this.render_json(content);
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break; break;
} }
let content_warning = html` let content_warning = html`
@ -604,108 +670,22 @@ ${JSON.stringify(content, null, 2)}</pre
? html` ${content_warning} ${content_html} ` ? html` ${content_warning} ${content_html} `
: content_warning : content_warning
: content_html; : content_html;
let is_encrypted = this.message?.decrypted return this.render_frame(html`
? html`<span style="align-self: center">🔓</span>` ${this.render_header()}
: undefined; <div class="w3-container">${payload}</div>
return html` ${this.render_votes()} ${this.render_actions()}
<style> </div>
code { `);
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px; text-wrap: nowrap"
><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${payload} ${this.render_votes()}
<footer class="w3-container">
${reply}
<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}
${this.render_children()}
</footer>
</div>
`;
} else if (content.type === 'issue') { } else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted return this.render_frame(html`
? html`<span style="align-self: center">🔓</span>` ${this.render_header()} ${content.text} ${this.render_votes()}
: undefined; <footer class="w3-container">
return html` <button class="w3-button w3-theme-d1" @click=${this.react}>
<style> React
code { </button>
white-space: pre-wrap; ${this.render_children()}
overflow-wrap: break-word; </footer>
} `);
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px; text-wrap: nowrap"
><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${content.text} ${this.render_votes()}
<footer class="w3-container">
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</footer>
</div>
`;
} else if (content.type === 'blog') { } else if (content.type === 'blog') {
let self = this; let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) { tfrpc.rpc.get_blob(content.blog).then(function (data) {
@ -741,77 +721,20 @@ ${JSON.stringify(content, null, 2)}</pre
`; `;
break; break;
} }
let reply = return this.render_frame(html`
this.drafts[this.message?.id] !== undefined ${this.render_header()}
? html` <div>${body}</div>
<tf-compose ${this.render_mentions()} ${this.render_votes()}
whoami=${this.whoami} ${this.render_actions()}
.users=${this.users} `);
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow: scroll; overflow-wrap: anywhere; display: block; max-width: 100%"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px; text-wrap: nowrap"
><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
<div>${body}</div>
${this.render_mentions()}
${this.render_votes()}
<footer class="w3-content">
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</footer>
</div>
`;
} else if (content.type === 'pub') { } else if (content.type === 'pub') {
return small_frame( return this.render_small_frame(
html` <style> html` <style>
span { span {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
</style> </style>
<span> <div class="w3-padding">
<div> <div>
🍻 🍻
<tf-user <tf-user
@ -820,38 +743,45 @@ ${JSON.stringify(content, null, 2)}</pre
></tf-user> ></tf-user>
</div> </div>
<pre>${content.address.host}:${content.address.port}</pre> <pre>${content.address.host}:${content.address.port}</pre>
</span>` </div>`
); );
} else if (content.type === 'channel') { } else if (content.type === 'channel') {
return small_frame(html` return this.render_small_frame(html`
<div> <div class="w3-container">
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <p>
<a href=${'#' + encodeURIComponent('#' + content.channel)} ${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
>#${content.channel}</a <a href=${'#' + encodeURIComponent('#' + content.channel)}
> >#${content.channel}</a
>
</p>
</div> </div>
`); `);
} else if (typeof this.message.content == 'string') { } else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) { if (this.message?.decrypted) {
if (this.format == 'decrypted') { if (this.format == 'decrypted') {
return small_frame( return this.render_small_frame(
html`<span>🔓</span> html`<span class="w3-container">🔓</span> ${this.render_json(
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>` this.message.decrypted
)}`
); );
} else { } else {
return small_frame( return this.render_small_frame(
html`<span>🔓</span> html`<span class="w3-container">🔓</span>
<div>${this.message.decrypted.type}</div>` <div class="w3-container">${this.message.decrypted.type}</div>`
); );
} }
} else { } else {
return small_frame(html`<span>🔒</span>`); return this.render_small_frame();
} }
} else { } else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`); return this.render_small_frame(
html`<div class="w3-container"><b>type</b>: ${content.type}</div>`
);
} }
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
} else { } else {
return small_frame(this.render_raw()); return this.render_small_frame(this.render_raw());
} }
} }
} }

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -198,7 +198,9 @@ class TfNewsElement extends LitElement {
} }
return html` return html`
<div> <div>
${final_messages.map( ${repeat(
final_messages,
(x) => x.id,
(x) => html` (x) => html`
<tf-message <tf-message
.message=${x} .message=${x}

View File

@ -11,7 +11,6 @@ class TfProfileElement extends LitElement {
id: {type: String}, id: {type: String},
users: {type: Object}, users: {type: Object},
size: {type: Number}, size: {type: Number},
server_follows_me: {type: Boolean},
following: {type: Boolean}, following: {type: Boolean},
blocking: {type: Boolean}, blocking: {type: Boolean},
}; };
@ -27,7 +26,6 @@ class TfProfileElement extends LitElement {
this.id = null; this.id = null;
this.users = {}; this.users = {};
this.size = 0; this.size = 0;
this.server_follows_me = undefined;
} }
async load() { async load() {
@ -63,26 +61,6 @@ class TfProfileElement extends LitElement {
} }
} }
async initial_load() {
this.server_follows_me = undefined;
let server_id = await tfrpc.rpc.getServerIdentity();
let followed = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages
WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`,
[server_id, this.whoami]
);
let is_followed = false;
for (let row of followed) {
is_followed = row.following != 0;
}
this.server_follows_me = is_followed;
}
modify(change) { modify(change) {
tfrpc.rpc tfrpc.rpc
.appendMessage( .appendMessage(
@ -175,31 +153,11 @@ class TfProfileElement extends LitElement {
input.click(); input.click();
} }
async server_follow_me(follow) {
try {
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
} catch (e) {
console.log(e);
}
try {
await this.initial_load();
} catch (e) {
console.log(e);
}
}
copy_id() { copy_id() {
navigator.clipboard.writeText(this.id); navigator.clipboard.writeText(this.id);
} }
render() { render() {
if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load();
}
this.load(); this.load();
let self = this; let self = this;
let profile = this.users[this.id] || {}; let profile = this.users[this.id] || {};
@ -216,22 +174,6 @@ class TfProfileElement extends LitElement {
let block; let block;
if (this.id === this.whoami) { if (this.id === this.whoami) {
if (this.editing) { if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html` edit = html`
<button <button
id="save_profile" id="save_profile"
@ -243,7 +185,6 @@ class TfProfileElement extends LitElement {
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard Discard
</button> </button>
${server_follow}
`; `;
} else { } else {
edit = html`<button edit = html`<button
@ -276,20 +217,18 @@ class TfProfileElement extends LitElement {
let edit_profile = this.editing let edit_profile = this.editing
? html` ? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px"> <div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div class="w3-container"> <div>
<div> <label for="name">Name:</label>
<label for="name">Name:</label> <input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))} placeholder="Choose a name"></input>
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input> </div>
</div> <div><label for="description">Description:</label></div>
<div><label for="description">Description:</label></div> <textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea> <div>
<div> <label for="public_web_hosting">Public Web Hosting:</label>
<label for="public_web_hosting">Public Web Hosting:</label> <input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input> </div>
</div> <div>
<div> <button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div> </div>
</div>` </div>`
: null; : null;
@ -302,12 +241,21 @@ class TfProfileElement extends LitElement {
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p> <p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p>
</header> </header>
<div class="w3-container"> <div class="w3-container">
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input> <div class="w3-margin-bottom" style="display: flex; flex-direction: row">
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button> <input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div style="display: flex; flex-direction: row; gap: 1em"> <div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile} ${edit_profile}
<div style="flex: 1 0 50%"> <div style="flex: 1 0 50%">
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div> ${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div> <div>${unsafeHTML(tfutils.markdown(description))}</div>
</div> </div>
</div> </div>

View File

@ -396,9 +396,9 @@ function is_dark(hex, value) {
function generated() { function generated() {
let now = new Date(); let now = new Date();
let k_color = rgb_to_hex([ let k_color = rgb_to_hex([
now.getDay() * 255 / 6, (now.getDay() * 128) / 6,
now.getHours() * 255 / 23, (now.getHours() * 128) / 23,
now.getSeconds() * 255 / 59, (now.getSeconds() * 128) / 59,
]); ]);
//let k_color = '#034f84'; //let k_color = '#034f84';
//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]); //let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);

View File

@ -142,12 +142,16 @@ class TfTabConnectionsElement extends LitElement {
}, {}) }, {})
); );
return html` return html`
<button ${connection.connected
class="w3-button w3-theme-d1" ? html`
@click=${() => tfrpc.rpc.closeConnection(connection.id)} <button
> class="w3-button w3-theme-d1"
Close @click=${() => tfrpc.rpc.closeConnection(connection.id)}
</button> >
Close
</button>
`
: undefined}
${connection.flags.one_shot ? '🔃' : undefined} ${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user> <tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ${connection.tunnel !== undefined
@ -264,24 +268,27 @@ class TfTabConnectionsElement extends LitElement {
)} )}
</ul> </ul>
<h2>Local Accounts</h2> <h2>Local Accounts</h2>
<ul class="w3-ul w3-border"> <div class="w3-container">
${this.identities.map( ${this.identities.map(
(x) => (x) =>
html`<li class="w3-bar"> html`<div
class="w3-tag w3-round w3-theme-l3"
style="padding: 4px; margin: 2px; max-width: 100%; text-wrap: nowrap; overflow: hidden"
>
${x == this.server_identity ${x == this.server_identity
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1" ? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
>🖥 local server</span 🖥 local server
>` </div>`
: undefined} : undefined}
${this.my_identities.indexOf(x) != -1 ${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1" ? html`<div class="w3-tag w3-medium w3-round w3-theme-d1">
>😎 you</span 😎 you
>` </div>`
: undefined} : undefined}
<tf-user id=${x} .users=${this.users}></tf-user> <tf-user id=${x} .users=${this.users}></tf-user>
</li>` </div>`
)} )}
</ul> </div>
</div> </div>
`; `;
} }

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -57,15 +57,14 @@ class TfTabNewsFeedElement extends LitElement {
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE WHERE
messages.author != ?1 AND messages.author != ?1 AND
messages.timestamp >= ?3 AND (?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20) ORDER BY timestamp DESC limit 20)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT * FROM mentions SELECT TRUE AS is_primary, * FROM mentions
`, `,
[ [
'"' + this.whoami.replace('"', '""') + '"', '"' + this.whoami.replace('"', '""') + '"',
@ -77,33 +76,29 @@ class TfTabNewsFeedElement extends LitElement {
} else if (this.hash.startsWith('#@')) { } else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature WITH
FROM messages selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
WHERE messages.author = ? FROM messages
ORDER BY sequence DESC) WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature ORDER BY sequence DESC LIMIT 20
FROM mine )
JOIN messages_refs ON mine.id = messages_refs.ref SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected
JOIN messages_refs ON selected.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
UNION UNION
SELECT * FROM mine SELECT TRUE AS is_primary, * FROM selected
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
`, `,
[this.hash.substring(1), start_time, end_time] [this.hash.substring(1), start_time, end_time]
); );
} else if (this.hash.startsWith('#%')) { } else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE id = ?1 WHERE messages.id = ?1
UNION UNION
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages JOIN messages_refs FROM messages JOIN messages_refs
ON messages.id = messages_refs.message ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1 WHERE messages_refs.ref = ?1
@ -117,34 +112,32 @@ class TfTabNewsFeedElement extends LitElement {
promises.push( promises.push(
tfrpc.rpc.query( 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 WITH
FROM messages all_news AS (
JOIN json_each(?) AS following ON messages.author = following.value SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
WHERE FROM messages
messages.timestamp >= ? AND JOIN json_each(?) AS following ON messages.author = following.value
messages.timestamp < ? AND WHERE messages.content ->> 'channel' = ?4
messages.content ->> 'channel' = ? UNION
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
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),
news AS (SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.ref JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.message JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id JOIN messages ON messages_refs.ref = messages.id
UNION UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT TRUE AS is_primary, news.* FROM news
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)), JSON.stringify(this.following.slice(i, i + k_following_limit)),
@ -160,63 +153,58 @@ class TfTabNewsFeedElement extends LitElement {
} else if (this.hash == '#🔐') { } else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value JOIN json_each(?1) AS following ON messages.author = following.value
WHERE WHERE
messages.timestamp >= ?2 AND (?2 IS NULL OR (messages.timestamp >= ?2) AND messages.timestamp < ?3 AND
messages.timestamp < ?3 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY sequence DESC ORDER BY sequence DESC LIMIT 20
`, `,
[JSON.stringify(this.following), start_time, end_time] [JSON.stringify(this.following), start_time, end_time]
); );
result = (await this.decrypt(result)).filter((x) => x.decrypted); result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else { } else {
let promises = []; result = await tfrpc.rpc.query(
const k_following_limit = 256; `
for (let i = 0; i < this.following.length; i += k_following_limit) { WITH
promises.push( all_news AS (
tfrpc.rpc.query( SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
`
WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages FROM messages
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp >= ? AND messages.timestamp < ? WHERE timestamp >= 0 AND timestamp < ?3),
ORDER BY messages.timestamp DESC) news AS (
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT * FROM all_news
FROM news WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
JOIN messages_refs ON news.id = messages_refs.ref ORDER BY timestamp DESC LIMIT 20
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 news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
start_time,
end_time,
]
) )
); SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
} FROM news
result = [].concat(...(await Promise.all(promises))); JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT TRUE AS is_primary, news.* FROM news
`,
[JSON.stringify(this.following), start_time, end_time]
);
} }
this.time_loading = undefined; this.time_loading = undefined;
return result; return result;
} }
update_time_range_from_messages(messages) { update_time_range_from_messages(messages) {
let only_primary = messages.filter((x) => x.is_primary);
this.time_range = [ this.time_range = [
messages.reduce( only_primary.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp), (accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0] this.time_range[0]
), ),
messages.reduce( only_primary.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp), (accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1] this.time_range[1]
), ),
@ -228,17 +216,11 @@ class TfTabNewsFeedElement extends LitElement {
this.loading_canceled = false; this.loading_canceled = false;
try { try {
let more = []; let more = [];
while (!more.length && !this.loading_canceled) { let last_start_time = this.time_range[0];
let last_start_time = this.start_time; more = await this.fetch_messages(null, last_start_time);
this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000; this.update_time_range_from_messages(
more = await this.fetch_messages(this.start_time, last_start_time); more.filter((x) => x.timestamp < 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]); this.messages = await this.decrypt([...more, ...this.messages]);
} finally { } finally {
this.loading--; this.loading--;
@ -274,6 +256,11 @@ class TfTabNewsFeedElement extends LitElement {
return result; return result;
} }
merge_messages(old_messages, new_messages) {
let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
}
async load_latest() { async load_latest() {
this.loading++; this.loading++;
let now = new Date().valueOf(); let now = new Date().valueOf();
@ -293,61 +280,48 @@ class TfTabNewsFeedElement extends LitElement {
} finally { } finally {
this.loading--; this.loading--;
} }
this.messages = Object.values( this.messages = this.merge_messages(
Object.fromEntries( this.messages,
[...this.messages, ...messages] Object.values(
.sort((x, y) => x.timestamp - y.timestamp) Object.fromEntries(
.slice(-1024) [...this.messages, ...messages]
.map((x) => [x.id, x]) .sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
) )
); );
console.log('done loading latest messages.'); console.log('done loading latest messages.');
} }
async load_messages() { async load_messages() {
let start_time = new Date();
let self = this; let self = this;
this.loading++; this.loading++;
let messages = []; let messages = [];
try { try {
this.messages = []; if (this._messages_hash !== this.hash) {
this._messages_hash = this.hash; this.messages = [];
this._messages_hash = this.hash;
}
this._messages_following = this.following; this._messages_following = this.following;
let now = new Date().valueOf(); let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000; let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time; this.start_time = start_time;
this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000]; this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages( messages = await this.fetch_messages(null, this.time_range[1]);
this.time_range[0],
this.time_range[1]
);
this.update_time_range_from_messages( this.update_time_range_from_messages(
messages.filter( messages.filter((x) => x.timestamp < this.time_range[1])
(x) =>
x.timestamp >= this.time_range[0] &&
x.timestamp < this.time_range[1]
)
); );
messages = await this.decrypt(messages); 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 { } finally {
this.loading--; this.loading--;
} }
this.messages = messages; this.messages = this.merge_messages(this.messages, messages);
this.time_loading = undefined; this.time_loading = undefined;
console.log(`loading messages done for ${self.whoami}`); console.log(
`loading messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
);
} }
mark_all_read() { mark_all_read() {
@ -373,7 +347,8 @@ class TfTabNewsFeedElement extends LitElement {
if ( if (
!this.messages || !this.messages ||
this._messages_hash !== this.hash || this._messages_hash !== this.hash ||
this._messages_following !== this.following JSON.stringify(this._messages_following) !==
JSON.stringify(this.following)
) { ) {
console.log( console.log(
`loading messages for ${this.whoami} (following ${this.following.length})` `loading messages for ${this.whoami} (following ${this.following.length})`
@ -417,7 +392,7 @@ class TfTabNewsFeedElement extends LitElement {
</p> </p>
`; `;
} }
return html` return cache(html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}> <button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read Mark All Read
</button> </button>
@ -433,7 +408,7 @@ class TfTabNewsFeedElement extends LitElement {
channel_unread=${this.channels_unread?.[this.channel()]} channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news> ></tf-news>
${more} ${more}
`; `);
} }
} }

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -128,6 +128,26 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
} }
compare_follows() {
const now = new Date().valueOf();
return function (a, b) {
return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
};
}
suggested_follows() {
/*
** Filter out people who have used future timestamps so that they aren't
** pinned at the top.
*/
let self = this;
return Object.entries(this.users)
.filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows())
.slice(0, 8)
.map((x) => x[0]);
}
render_sidebar() { render_sidebar() {
return html` return html`
<div <div
@ -153,7 +173,7 @@ class TfTabNewsElement extends LitElement {
> >
` `
: undefined} : undefined}
<div class="w3-bar-item w3-theme-d2">Channels</div> <h4 class="w3-bar-item w3-theme-d2">Channels</h4>
<a <a
href="#" href="#"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
@ -195,10 +215,30 @@ class TfTabNewsElement extends LitElement {
` `
)} )}
<div class="w3-bar-item w3-theme-d2">Connections</div> <h4 class="w3-bar-item w3-theme-d2">Connections</h4>
${this.connections.map((x) => (html` ${this.connections
<tf-user class="w3-bar-item" style="max-width: 100%" id=${x.id} .users=${this.users}></tf-user> .filter((x) => x.id && !x.destroy_reason)
`))} .map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x.id}
.users=${this.users}
></tf-user>
`
)}
<h4 class="w3-bar-item w3-theme-d2">Suggested Follows</h4>
${this.suggested_follows().map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x}
.users=${this.users}
></tf-user>
`
)}
</div> </div>
<div <div
class="w3-overlay" class="w3-overlay"
@ -231,10 +271,10 @@ class TfTabNewsElement extends LitElement {
name. name.
</div>`; </div>`;
} }
return html` return cache(html`
${this.render_sidebar()} ${this.render_sidebar()}
<div <div
style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: scroll" style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
id="main" id="main"
class="w3-main" class="w3-main"
> >
@ -290,7 +330,7 @@ class TfTabNewsElement extends LitElement {
></tf-tab-news-feed> ></tf-tab-news-feed>
</div> </div>
</div> </div>
`; `);
} }
} }

View File

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

View File

@ -19,27 +19,33 @@ class TfUserElement extends LitElement {
} }
render() { render() {
let user = this.users[this.id];
let shape = !user || user.follow_depth >= 2 ? 'w3-circle' : 'w3-round';
let image = html`<span let image = html`<span
class="w3-theme-light w3-circle" class=${'w3-theme-l4 ' + shape}
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em" style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
>?</span >😎</span
>`; >`;
let name = this.users?.[this.id]?.name; let name = this.users?.[this.id]?.name;
name = html`<a target="_top" href=${'#' + this.id}>${name !== undefined ? name : this.id}</a>` name = html`<a target="_top" href=${'#' + this.id}
>${name !== undefined ? name : this.id}</a
>`;
if (this.users[this.id]) { if (user) {
let image_link = this.users[this.id].image; let image_link = user.image;
image_link = image_link =
typeof image_link == 'string' ? image_link : image_link?.link; typeof image_link == 'string' ? image_link : image_link?.link;
if (image_link !== undefined) { if (image_link !== undefined) {
image = html`<img image = html`<img
class="w3-circle" class=${'w3-theme-l4 ' + shape}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover" style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
src="/${image_link}/view" src="/${image_link}/view"
/>`; />`;
} }
} }
return html` <div style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"> return html` <div
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
>
${image} ${name} ${image} ${name}
</div>`; </div>`;
} }

View File

@ -149,7 +149,7 @@ class TfNavigationElement extends LitElement {
<link type="text/css" rel="stylesheet" href="/static/w3.css" /> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%"> <div class="w3-dropdown-click w3-right" style="max-width: 100%">
<button <button
class="w3-button w3-rest w3-cyan" class="w3-button w3-rest"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
id="identity" id="identity"
@click=${self.toggle_id_dropdown} @click=${self.toggle_id_dropdown}

View File

@ -577,19 +577,6 @@ async function getProcessBlob(blobId, key, options) {
); );
} }
}; };
imports.ssb.setServerFollowingMe = function (id, following) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.setServerFollowingMe(
process.credentials.session.name,
id,
following
);
}
};
imports.ssb.swapWithServerIdentity = function (id) { imports.ssb.swapWithServerIdentity = function (id) {
if ( if (
process.credentials && process.credentials &&

View File

@ -38,8 +38,8 @@
style=" style="
display: flex; display: flex;
flex-flow: column; flex-flow: column;
width: 100vw; width: 100%;
height: 100vh; height: 100%;
position: absolute; position: absolute;
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;

View File

@ -7,8 +7,8 @@ html {
body { body {
font-family: monospace; font-family: monospace;
background-color: #002b36; background-color: #444;
color: #eee8d5; color: #fff;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;

File diff suppressed because one or more lines are too long

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

@ -22,7 +22,6 @@
"version": "6.18.4", "version": "6.18.4",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
"integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==", "integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -31,10 +30,9 @@
} }
}, },
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.7.1", "version": "6.8.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
"integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
@ -46,7 +44,6 @@
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
@ -59,7 +56,6 @@
"version": "6.4.9", "version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0", "@codemirror/lang-css": "^6.0.0",
@ -76,7 +72,6 @@
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
@ -91,17 +86,15 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.7", "version": "6.10.8",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.7.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz",
"integrity": "sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==", "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@ -115,7 +108,6 @@
"version": "6.8.4", "version": "6.8.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0", "@codemirror/view": "^6.35.0",
@ -126,7 +118,6 @@
"version": "6.5.8", "version": "6.5.8",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz",
"integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==", "integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
@ -134,10 +125,9 @@
} }
}, },
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
"version": "6.5.0", "version": "6.5.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.1.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", "integrity": "sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@ -146,7 +136,6 @@
"version": "6.1.2", "version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -155,10 +144,9 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.36.1", "version": "6.36.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.2.tgz",
"integrity": "sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==", "integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
@ -170,7 +158,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.2.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
@ -185,7 +172,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
@ -195,7 +181,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
@ -205,7 +190,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25" "@jridgewell/trace-mapping": "^0.3.25"
@ -215,15 +199,13 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
@ -232,14 +214,12 @@
"node_modules/@lezer/common": { "node_modules/@lezer/common": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
"license": "MIT"
}, },
"node_modules/@lezer/css": { "node_modules/@lezer/css": {
"version": "1.1.9", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==", "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@ -250,7 +230,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@ -259,7 +238,6 @@
"version": "1.3.10", "version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@ -270,7 +248,6 @@
"version": "1.4.21", "version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==", "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
@ -278,10 +255,9 @@
} }
}, },
"node_modules/@lezer/json": { "node_modules/@lezer/json": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@ -292,7 +268,6 @@
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@ -300,14 +275,12 @@
"node_modules/@marijn/find-cluster-break": { "node_modules/@marijn/find-cluster-break": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
"license": "MIT"
}, },
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2", "@types/resolve": "1.20.2",
@ -332,7 +305,6 @@
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.1",
"smob": "^1.0.0", "smob": "^1.0.0",
@ -354,7 +326,6 @@
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.0", "@types/estree": "^1.0.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
@ -373,247 +344,228 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
"integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
"integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
"integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
"integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
"integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
"integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
"integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
"integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
"integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
"integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
"integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
"integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
"integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
"integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
"integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
"integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
"integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
"integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -622,21 +574,18 @@
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
"license": "MIT"
}, },
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
"license": "MIT"
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -648,14 +597,12 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/codemirror": { "node_modules/codemirror": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0", "@codemirror/commands": "^6.0.0",
@ -670,20 +617,17 @@
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/crelt": { "node_modules/crelt": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
"license": "MIT"
}, },
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -691,15 +635,13 @@
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
"license": "MIT"
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -712,7 +654,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@ -721,7 +662,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@ -733,7 +673,6 @@
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": { "dependencies": {
"hasown": "^2.0.2" "hasown": "^2.0.2"
}, },
@ -747,20 +686,17 @@
"node_modules/is-module": { "node_modules/is-module": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
"license": "MIT"
}, },
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
"license": "MIT"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -773,7 +709,6 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
@ -782,7 +717,6 @@
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.16.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
@ -799,10 +733,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.29.1", "version": "4.31.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
"integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.6" "@types/estree": "1.0.6"
}, },
@ -814,25 +747,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.29.1", "@rollup/rollup-android-arm-eabi": "4.31.0",
"@rollup/rollup-android-arm64": "4.29.1", "@rollup/rollup-android-arm64": "4.31.0",
"@rollup/rollup-darwin-arm64": "4.29.1", "@rollup/rollup-darwin-arm64": "4.31.0",
"@rollup/rollup-darwin-x64": "4.29.1", "@rollup/rollup-darwin-x64": "4.31.0",
"@rollup/rollup-freebsd-arm64": "4.29.1", "@rollup/rollup-freebsd-arm64": "4.31.0",
"@rollup/rollup-freebsd-x64": "4.29.1", "@rollup/rollup-freebsd-x64": "4.31.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.29.1", "@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
"@rollup/rollup-linux-arm-musleabihf": "4.29.1", "@rollup/rollup-linux-arm-musleabihf": "4.31.0",
"@rollup/rollup-linux-arm64-gnu": "4.29.1", "@rollup/rollup-linux-arm64-gnu": "4.31.0",
"@rollup/rollup-linux-arm64-musl": "4.29.1", "@rollup/rollup-linux-arm64-musl": "4.31.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.29.1", "@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
"@rollup/rollup-linux-riscv64-gnu": "4.29.1", "@rollup/rollup-linux-riscv64-gnu": "4.31.0",
"@rollup/rollup-linux-s390x-gnu": "4.29.1", "@rollup/rollup-linux-s390x-gnu": "4.31.0",
"@rollup/rollup-linux-x64-gnu": "4.29.1", "@rollup/rollup-linux-x64-gnu": "4.31.0",
"@rollup/rollup-linux-x64-musl": "4.29.1", "@rollup/rollup-linux-x64-musl": "4.31.0",
"@rollup/rollup-win32-arm64-msvc": "4.29.1", "@rollup/rollup-win32-arm64-msvc": "4.31.0",
"@rollup/rollup-win32-ia32-msvc": "4.29.1", "@rollup/rollup-win32-ia32-msvc": "4.31.0",
"@rollup/rollup-win32-x64-msvc": "4.29.1", "@rollup/rollup-win32-x64-msvc": "4.31.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -854,15 +787,13 @@
"type": "consulting", "type": "consulting",
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
], ]
"license": "MIT"
}, },
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
@ -871,15 +802,13 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -889,7 +818,6 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
@ -898,14 +826,12 @@
"node_modules/style-mod": { "node_modules/style-mod": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
"license": "MIT"
}, },
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -918,7 +844,6 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.8.2",
@ -935,8 +860,7 @@
"node_modules/w3c-keyname": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
"license": "MIT"
} }
} }
} }

2
deps/libuv vendored

@ -1 +1 @@
Subproject commit e1095c7a4373ce00cd8874d8e820de5afb25776e Subproject commit 8fb9cb919489a48880680a56efecff6a7dfb4504

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
(()=>{var D="./favicon-16x16-VSI62OPJ.png";})();
//# sourceMappingURL=favicon-16x16-V2DMIAZS.js.map

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,2 @@
(()=>{var T="./favicon-32x32-3EB2YCUY.png";})();
//# sourceMappingURL=favicon-32x32-THY3JDJL.js.map

BIN
deps/speedscope/favicon-FOKUP5Y5.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

2
deps/speedscope/favicon-M34RF7BI.js vendored Normal file
View File

@ -0,0 +1,2 @@
(()=>{var m="./favicon-FOKUP5Y5.ico";})();
//# sourceMappingURL=favicon-M34RF7BI.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,19 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>speedscope</title><link href="source-code-pro.52b1676f.css" rel="stylesheet"><script></script><link rel="stylesheet" href="reset.8c46b7a1.css"><link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.bc503437.png"><link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.f74b3187.png"></head><body> <script src="speedscope.6f107512.js"></script> <!DOCTYPE html>
</body></html> <html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>speedscope</title>
<link rel="stylesheet" href="speedscope-GHPHNKXC.css">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32-3EB2YCUY.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16-VSI62OPJ.png">
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head>
<body>
<script src="speedscope-CAEVGCWN.js"></script>
</body>
</html>

View File

@ -1,3 +1,3 @@
speedscope@1.21.0 speedscope@1.22.0
Sat Nov 16 22:13:27 PST 2024 Thu Jan 16 16:49:47 PST 2025
d36c3a54424063a8df7bc67a7b824a223d73861b bd7b44a0a7d63375ee6ea0a0d1b96e65a456642f

View File

@ -1,2 +0,0 @@
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:initial}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden}body,html{height:100%}body{overflow:auto}
/*# sourceMappingURL=reset.8c46b7a1.css.map */

View File

@ -1,2 +0,0 @@
@font-face{font-family:Source Code Pro;font-weight:400;font-style:normal;font-stretch:normal;src:url(SourceCodePro-Regular.ttf.f546cbe0.woff2) format("woff2")}
/*# sourceMappingURL=source-code-pro.52b1676f.css.map */

File diff suppressed because one or more lines are too long

189
deps/speedscope/speedscope-CAEVGCWN.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden;height:100%}body{height:100%;overflow:auto}@font-face{font-family:Source Code Pro;font-weight:400;font-style:normal;font-stretch:normal;src:url("./SourceCodePro-Regular.ttf-ILST5JV6.woff2") format("woff2")}
/*# sourceMappingURL=speedscope-GHPHNKXC.css.map */

File diff suppressed because one or more lines are too long

638
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

1927
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

53
deps/sqlite/sqlite3.h vendored
View File

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.47.2" #define SQLITE_VERSION "3.48.0"
#define SQLITE_VERSION_NUMBER 3047002 #define SQLITE_VERSION_NUMBER 3048000
#define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c" #define SQLITE_SOURCE_ID "2025-01-14 11:05:00 d2fe6b05f38d9d7cd78c5d252e99ac59f1aea071d669830c1ffe4e8966e84010"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -1100,6 +1100,11 @@ struct sqlite3_io_methods {
** pointed to by the pArg argument. This capability is used during testing ** pointed to by the pArg argument. This capability is used during testing
** and only needs to be supported when SQLITE_TEST is defined. ** and only needs to be supported when SQLITE_TEST is defined.
** **
** <li>[[SQLITE_FCNTL_NULL_IO]]
** The [SQLITE_FCNTL_NULL_IO] opcode sets the low-level file descriptor
** or file handle for the [sqlite3_file] object such that it will no longer
** read or write to the database file.
**
** <li>[[SQLITE_FCNTL_WAL_BLOCK]] ** <li>[[SQLITE_FCNTL_WAL_BLOCK]]
** The [SQLITE_FCNTL_WAL_BLOCK] is a signal to the VFS layer that it might ** The [SQLITE_FCNTL_WAL_BLOCK] is a signal to the VFS layer that it might
** be advantageous to block on the next WAL lock if the lock is not immediately ** be advantageous to block on the next WAL lock if the lock is not immediately
@ -1253,6 +1258,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_EXTERNAL_READER 40 #define SQLITE_FCNTL_EXTERNAL_READER 40
#define SQLITE_FCNTL_CKSM_FILE 41 #define SQLITE_FCNTL_CKSM_FILE 41
#define SQLITE_FCNTL_RESET_CACHE 42 #define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43
/* deprecated names */ /* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@ -2631,10 +2637,14 @@ SQLITE_API void sqlite3_set_last_insert_rowid(sqlite3*,sqlite3_int64);
** deleted by the most recently completed INSERT, UPDATE or DELETE ** deleted by the most recently completed INSERT, UPDATE or DELETE
** statement on the database connection specified by the only parameter. ** statement on the database connection specified by the only parameter.
** The two functions are identical except for the type of the return value ** The two functions are identical except for the type of the return value
** and that if the number of rows modified by the most recent INSERT, UPDATE ** and that if the number of rows modified by the most recent INSERT, UPDATE,
** or DELETE is greater than the maximum value supported by type "int", then ** or DELETE is greater than the maximum value supported by type "int", then
** the return value of sqlite3_changes() is undefined. ^Executing any other ** the return value of sqlite3_changes() is undefined. ^Executing any other
** type of SQL statement does not modify the value returned by these functions. ** type of SQL statement does not modify the value returned by these functions.
** For the purposes of this interface, a CREATE TABLE AS SELECT statement
** does not count as an INSERT, UPDATE or DELETE statement and hence the rows
** added to the new table by the CREATE TABLE AS SELECT statement are not
** counted.
** **
** ^Only changes made directly by the INSERT, UPDATE or DELETE statement are ** ^Only changes made directly by the INSERT, UPDATE or DELETE statement are
** considered - auxiliary changes caused by [CREATE TRIGGER | triggers], ** considered - auxiliary changes caused by [CREATE TRIGGER | triggers],
@ -4194,11 +4204,22 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
** <dd>The SQLITE_PREPARE_NO_VTAB flag causes the SQL compiler ** <dd>The SQLITE_PREPARE_NO_VTAB flag causes the SQL compiler
** to return an error (error code SQLITE_ERROR) if the statement uses ** to return an error (error code SQLITE_ERROR) if the statement uses
** any virtual tables. ** any virtual tables.
**
** [[SQLITE_PREPARE_DONT_LOG]] <dt>SQLITE_PREPARE_DONT_LOG</dt>
** <dd>The SQLITE_PREPARE_DONT_LOG flag prevents SQL compiler
** errors from being sent to the error log defined by
** [SQLITE_CONFIG_LOG]. This can be used, for example, to do test
** compiles to see if some SQL syntax is well-formed, without generating
** messages on the global error log when it is not. If the test compile
** fails, the sqlite3_prepare_v3() call returns the same error indications
** with or without this flag; it just omits the call to [sqlite3_log()] that
** logs the error.
** </dl> ** </dl>
*/ */
#define SQLITE_PREPARE_PERSISTENT 0x01 #define SQLITE_PREPARE_PERSISTENT 0x01
#define SQLITE_PREPARE_NORMALIZE 0x02 #define SQLITE_PREPARE_NORMALIZE 0x02
#define SQLITE_PREPARE_NO_VTAB 0x04 #define SQLITE_PREPARE_NO_VTAB 0x04
#define SQLITE_PREPARE_DONT_LOG 0x10
/* /*
** CAPI3REF: Compiling An SQL Statement ** CAPI3REF: Compiling An SQL Statement
@ -10889,7 +10910,7 @@ SQLITE_API int sqlite3_deserialize(
#ifdef __cplusplus #ifdef __cplusplus
} /* End of the 'extern "C"' block */ } /* End of the 'extern "C"' block */
#endif #endif
#endif /* SQLITE3_H */ /* #endif for SQLITE3_H will be added by mksqlite3.tcl */
/******** Begin file sqlite3rtree.h *********/ /******** Begin file sqlite3rtree.h *********/
/* /*
@ -13140,14 +13161,29 @@ struct Fts5PhraseIter {
** value returned by xInstCount(), SQLITE_RANGE is returned. Otherwise, ** value returned by xInstCount(), SQLITE_RANGE is returned. Otherwise,
** output variable (*ppToken) is set to point to a buffer containing the ** output variable (*ppToken) is set to point to a buffer containing the
** matching document token, and (*pnToken) to the size of that buffer in ** matching document token, and (*pnToken) to the size of that buffer in
** bytes. This API is not available if the specified token matches a ** bytes.
** prefix query term. In that case both output variables are always set
** to 0.
** **
** The output text is not a copy of the document text that was tokenized. ** The output text is not a copy of the document text that was tokenized.
** It is the output of the tokenizer module. For tokendata=1 tables, this ** It is the output of the tokenizer module. For tokendata=1 tables, this
** includes any embedded 0x00 and trailing data. ** includes any embedded 0x00 and trailing data.
** **
** This API may be slow in some cases if the token identified by parameters
** iIdx and iToken matched a prefix token in the query. In most cases, the
** first call to this API for each prefix token in the query is forced
** to scan the portion of the full-text index that matches the prefix
** token to collect the extra data required by this API. If the prefix
** token matches a large number of token instances in the document set,
** this may be a performance problem.
**
** If the user knows in advance that a query may use this API for a
** prefix token, FTS5 may be configured to collect all required data as part
** of the initial querying of the full-text index, avoiding the second scan
** entirely. This also causes prefix queries that do not use this API to
** run more slowly and use more memory. FTS5 may be configured in this way
** either on a per-table basis using the [FTS5 insttoken | 'insttoken']
** option, or on a per-query basis using the
** [fts5_insttoken | fts5_insttoken()] user function.
**
** This API can be quite slow if used with an FTS5 table created with the ** This API can be quite slow if used with an FTS5 table created with the
** "detail=none" or "detail=column" option. ** "detail=none" or "detail=column" option.
** **
@ -13581,3 +13617,4 @@ struct fts5_api {
#endif /* _FTS5_H */ #endif /* _FTS5_H */
/******** End of fts5.h *********/ /******** End of fts5.h *********/
#endif /* SQLITE3_H */

View File

@ -0,0 +1,21 @@
* Fix win32 crashes.
* Cross-compile for aarch64.
* Added CLI commands.
* SSB
* Invites.
* Replying in a new thread.
* Drafts in the sidebar.
* Fix channel cycling shortcut.
* New databases go in ~/.local/share/tildefriends/.
* Show disconnect reasons.
* Improved unread status.
* Improved Replication.
* Show messages as they arrive.
* Faster load.
* Dynamic colors.
* Loading screen.
* Updated:
* CodeMirror
* libuv 1.50.0
* speedscope 1.22.0
* sqlite 3.48.0

1
package-lock.json generated
View File

@ -14,7 +14,6 @@
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },

View File

@ -629,6 +629,12 @@ static void _http_on_connection(uv_stream_t* stream, int status)
{ {
tf_http_listener_t* listener = stream->data; tf_http_listener_t* listener = stream->data;
tf_http_t* http = listener->http; tf_http_t* http = listener->http;
if (http->is_shutting_down)
{
tf_printf("Ignoring HTTP connection during shutdown.\n");
return;
}
tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t)); tf_http_connection_t* connection = tf_malloc(sizeof(tf_http_connection_t));
*connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection }, .is_receiving_headers = true }; *connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection }, .is_receiving_headers = true };
if (listener->tls) if (listener->tls)

View File

@ -15,6 +15,7 @@
#include "unzip.h" #include "unzip.h"
#include <getopt.h> #include <getopt.h>
#include <inttypes.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -42,11 +43,11 @@
struct backtrace_state* g_backtrace_state; struct backtrace_state* g_backtrace_state;
#if !defined(TARGET_OS_IPHONE) #if !TARGET_OS_IPHONE
static const char* _get_db_path() static const char* _get_db_path()
{ {
const char* k_db_path_default = "db.sqlite"; const char* k_db_path_default = "db.sqlite";
#if defined(__linux__) #if defined(__linux__) && !defined(__ANDROID__)
if (stat(k_db_path_default, &(struct stat) { 0 }) == 0) if (stat(k_db_path_default, &(struct stat) { 0 }) == 0)
{ {
return tf_strdup(k_db_path_default); return tf_strdup(k_db_path_default);
@ -143,9 +144,16 @@ static void _create_directories_for_file(const char* path, int mode)
static int _tf_command_export(const char* file, int argc, char* argv[]); static int _tf_command_export(const char* file, int argc, char* argv[]);
static int _tf_command_import(const char* file, int argc, char* argv[]); static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_publish(const char* file, int argc, char* argv[]); static int _tf_command_publish(const char* file, int argc, char* argv[]);
static int _tf_command_private(const char* file, int argc, char* argv[]);
static int _tf_command_run(const char* file, int argc, char* argv[]); static int _tf_command_run(const char* file, int argc, char* argv[]);
static int _tf_command_sandbox(const char* file, int argc, char* argv[]); static int _tf_command_sandbox(const char* file, int argc, char* argv[]);
static int _tf_command_has_blob(const char* file, int argc, char* argv[]);
static int _tf_command_store_blob(const char* file, int argc, char* argv[]); static int _tf_command_store_blob(const char* file, int argc, char* argv[]);
static int _tf_command_create_invite(const char* file, int argc, char* argv[]);
static int _tf_command_get_sequence(const char* file, int argc, char* argv[]);
static int _tf_command_get_identity(const char* file, int argc, char* argv[]);
static int _tf_command_get_profile(const char* file, int argc, char* argv[]);
static int _tf_command_get_contacts(const char* file, int argc, char* argv[]);
static int _tf_command_test(const char* file, int argc, char* argv[]); static int _tf_command_test(const char* file, int argc, char* argv[]);
static int _tf_command_verify(const char* file, int argc, char* argv[]); static int _tf_command_verify(const char* file, int argc, char* argv[]);
static int _tf_command_usage(const char* file); static int _tf_command_usage(const char* file);
@ -163,6 +171,13 @@ const command_t k_commands[] = {
{ "import", _tf_command_import, "Import apps to SSB." }, { "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." }, { "export", _tf_command_export, "Export apps from SSB." },
{ "publish", _tf_command_publish, "Append a message to a feed." }, { "publish", _tf_command_publish, "Append a message to a feed." },
{ "private", _tf_command_private, "Append a private post message to a feed." },
{ "create_invite", _tf_command_create_invite, "Create an invite." },
{ "get_sequence", _tf_command_get_sequence, "Get the last sequence number for a feed." },
{ "get_identity", _tf_command_get_identity, "Get the server account identity." },
{ "get_profile", _tf_command_get_profile, "Get profile information for the given identity." },
{ "get_contacts", _tf_command_get_contacts, "Get information about followed, blocked, and friend identities." },
{ "has_blob", _tf_command_has_blob, "Check whether a blob is in the blob store." },
{ "store_blob", _tf_command_store_blob, "Write a file to the blob store." }, { "store_blob", _tf_command_store_blob, "Write a file to the blob store." },
{ "verify", _tf_command_verify, "Verify a feed." }, { "verify", _tf_command_verify, "Verify a feed." },
{ "test", _tf_command_test, "Test SSB." }, { "test", _tf_command_test, "Test SSB." },
@ -274,6 +289,7 @@ static int _tf_command_import(const char* file, int argc, char* argv[])
_create_directories_for_file(db_path, 0700); _create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
if (optind < argc) if (optind < argc)
{ {
for (int i = optind; i < argc; i++) for (int i = optind; i < argc; i++)
@ -343,6 +359,7 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
} }
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
if (optind < argc) if (optind < argc)
{ {
for (int i = optind; i < argc; i++) for (int i = optind; i < argc; i++)
@ -461,6 +478,7 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user); tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user);
_create_directories_for_file(db_path, 0700); _create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
uint8_t private_key[512] = { 0 }; uint8_t private_key[512] = { 0 };
if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key))) if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
{ {
@ -496,6 +514,140 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
return result; return result;
} }
static int _tf_command_private(const char* file, int argc, char* argv[])
{
const char* user = NULL;
const char* identity = NULL;
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* text = NULL;
const char* recipients = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "user", required_argument, NULL, 'u' },
{ "id", required_argument, NULL, 'i' },
{ "recipients", required_argument, NULL, 'r' },
{ "db-path", required_argument, NULL, 'd' },
{ "text", required_argument, NULL, 'c' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "u:i:d:t:r:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'u':
user = optarg;
break;
case 'i':
identity = optarg;
break;
case 'd':
db_path = optarg;
break;
case 't':
text = optarg;
break;
case 'r':
recipients = optarg;
break;
}
}
if (show_usage || !user || !identity || !recipients || !text)
{
tf_printf("\n%s private [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -u, --user user User owning identity with which to publish.\n");
tf_printf(" -i, --id identity Identity with which to publish message.\n");
tf_printf(" -r, --recipients recipients Recipient identities.\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -t, --text text Private post text.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
int result = EXIT_FAILURE;
tf_printf("Posting %s as account %s belonging to %s...\n", text, identity, user);
_create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
uint8_t private_key[512] = { 0 };
const char* recipient_list[k_max_private_message_recipients] = { 0 };
int recipient_count = 0;
recipient_list[recipient_count++] = identity;
if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
{
char* copy = tf_strdup(recipients);
char* next = NULL;
const char* it = strtok_r(copy, ",", &next);
while (it)
{
if (recipient_count == k_max_private_message_recipients)
{
tf_printf("Too many recipients (max %d).\n", k_max_private_message_recipients);
goto done;
}
recipient_list[recipient_count++] = it;
it = strtok_r(NULL, ",", &next);
}
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "post"));
JS_SetPropertyStr(context, message, "text", JS_NewString(context, text));
JSValue recps = JS_NewArray(context);
for (int i = 0; i < recipient_count; i++)
{
JS_SetPropertyUint32(context, recps, i, JS_NewString(context, recipient_list[i]));
}
JS_SetPropertyStr(context, message, "recps", recps);
JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
const char* message_str = JS_ToCString(context, json);
char* encrypted = tf_ssb_private_message_encrypt(private_key, recipient_list, recipient_count, message_str, strlen(message_str));
if (encrypted)
{
int64_t sequence = 0;
char previous[k_id_base64_len] = { 0 };
tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, previous, sizeof(previous));
JSValue content = JS_NewString(context, encrypted);
JSValue to_publish = tf_ssb_sign_message(ssb, identity, private_key, content, previous, sequence);
tf_ssb_verify_strip_and_store_message(ssb, to_publish, _tf_published_callback, &result);
JS_FreeValue(context, to_publish);
JS_FreeValue(context, content);
}
tf_free(encrypted);
JS_FreeCString(context, message_str);
JS_FreeValue(context, json);
JS_FreeValue(context, message);
tf_free(copy);
}
else
{
tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user);
}
done:
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_store_blob(const char* file, int argc, char* argv[]) static int _tf_command_store_blob(const char* file, int argc, char* argv[])
{ {
const char* default_db_path = _get_db_path(); const char* default_db_path = _get_db_path();
@ -582,6 +734,7 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
char id[256]; char id[256];
_create_directories_for_file(db_path, 0700); _create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL)) if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL))
{ {
tf_printf("%s\n", id); tf_printf("%s\n", id);
@ -592,6 +745,417 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
static int _tf_command_has_blob(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* blob_id = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "blob_id", required_argument, NULL, 'b' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:b:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'b':
blob_id = optarg;
break;
}
}
if (show_usage || !blob_id)
{
tf_printf("\n%s has_blob [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -b, --blob_id blob_id ID of blob to query.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
sqlite3* db = NULL;
sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
tf_ssb_db_init_reader(db);
bool has = tf_ssb_db_blob_has(db, blob_id);
sqlite3_close(db);
tf_free((void*)default_db_path);
tf_printf("%s\n", has ? "true" : "false");
return has ? EXIT_SUCCESS : EXIT_FAILURE;
}
static int _tf_command_create_invite(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
int use_count = 0;
int expires = 0;
bool show_usage = false;
const char* host = NULL;
int port = 0;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "identity", required_argument, NULL, 'i' },
{ "address", required_argument, NULL, 'a' },
{ "port", required_argument, NULL, 'p' },
{ "use_count", required_argument, NULL, 'u' },
{ "expires", required_argument, NULL, 'e' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:a:p:u:e:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
case 'a':
host = optarg;
break;
case 'p':
port = atoi(optarg);
break;
case 'u':
use_count = atoi(optarg);
break;
case 'e':
expires = atoi(optarg);
break;
}
}
if (show_usage || !identity || !use_count || !expires || !host || !port)
{
tf_printf("\n%s get_sequence [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
tf_printf(" -a, --address address Address to which the recipient will connect.\n");
tf_printf(" -p, --port port Port to which the recipient will connect.\n");
tf_printf(" -u, --use_count count Number of times this invite may be used.\n");
tf_printf(" -e, --expires seconds How long this invite is valid in seconds (-1 for indefinitely).\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
int result = EXIT_FAILURE;
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
char invite[1024] = "";
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (tf_ssb_db_generate_invite(db, identity, host, port, use_count, expires, invite, sizeof(invite)))
{
tf_printf("%s\n", invite);
result = EXIT_SUCCESS;
}
tf_ssb_release_db_writer(ssb, db);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_get_sequence(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "id", required_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
}
}
if (show_usage || !identity)
{
tf_printf("\n%s get_sequence [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
int64_t sequence = -1;
int result = tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, NULL, 0) ? EXIT_SUCCESS : EXIT_FAILURE;
tf_printf("%" PRId64 "\n", sequence);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_get_identity(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
}
}
if (show_usage)
{
tf_printf("\n%s get_identity [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
char id[k_id_base64_len] = { 0 };
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
int result = tf_ssb_whoami(ssb, id, sizeof(id)) ? EXIT_SUCCESS : EXIT_FAILURE;
tf_printf("%s\n", id);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_get_profile(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "id", required_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
}
}
if (show_usage || !identity)
{
tf_printf("\n%s get_profile [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account for which to get profile information.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
sqlite3* db = NULL;
sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
tf_ssb_db_init_reader(db);
const char* profile = tf_ssb_db_get_profile(db, identity);
tf_printf("%s\n", profile);
sqlite3_close(db);
tf_free((void*)profile);
tf_free((void*)default_db_path);
return profile != NULL;
}
static int _tf_command_get_contacts(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "id", required_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
}
}
if (show_usage || !identity)
{
tf_printf("\n%s get_contacts [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get contact information.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
JSContext* context = tf_ssb_get_context(ssb);
JSValue contacts = JS_NewObject(context);
JSValue follows = JS_NewObject(context);
JSValue blocks = JS_NewObject(context);
JSValue friends = JS_NewObject(context);
tf_ssb_following_t* following = tf_ssb_db_following_deep(ssb, &identity, 1, 1, true);
tf_ssb_following_t* following2 = tf_ssb_db_following_deep(ssb, &identity, 1, 2, false);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
for (int i = 0; *following[i].id; i++)
{
if (following[i].followed_by_count)
{
const char* name = tf_ssb_db_get_profile_name(db, following[i].id);
JS_SetPropertyStr(context, follows, following[i].id, name ? JS_NewString(context, name) : JS_NULL);
tf_free((void*)name);
}
if (following[i].blocked_by_count)
{
const char* name = tf_ssb_db_get_profile_name(db, following[i].id);
JS_SetPropertyStr(context, blocks, following[i].id, name ? JS_NewString(context, name) : JS_NULL);
tf_free((void*)name);
}
}
for (int i = 0; *following2[i].id; i++)
{
const char* name = tf_ssb_db_get_profile_name(db, following2[i].id);
JS_SetPropertyStr(context, friends, following2[i].id, name ? JS_NewString(context, name) : JS_NULL);
tf_free((void*)name);
}
tf_ssb_release_db_reader(ssb, db);
JS_SetPropertyStr(context, contacts, "follows", follows);
JS_SetPropertyStr(context, contacts, "blocks", blocks);
JS_SetPropertyStr(context, contacts, "friends", friends);
tf_free(following2);
tf_free(following);
JSValue json = JS_JSONStringify(context, contacts, JS_NULL, JS_NewInt32(context, 2));
const char* json_str = JS_ToCString(context, json);
tf_printf("%s\n", json_str);
JS_FreeCString(context, json_str);
JS_FreeValue(context, json);
JS_FreeValue(context, contacts);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return EXIT_SUCCESS;
}
static int _tf_command_verify(const char* file, int argc, char* argv[]) static int _tf_command_verify(const char* file, int argc, char* argv[])
{ {
const char* identity = NULL; const char* identity = NULL;
@ -1008,6 +1572,21 @@ static void _error_handler(int sig)
_exit(1); _exit(1);
} }
#if defined(_WIN32)
static LONG WINAPI _win32_exception_handler(EXCEPTION_POINTERS* info)
{
if (info->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION || info->ExceptionRecord->ExceptionCode == STATUS_ILLEGAL_INSTRUCTION ||
info->ExceptionRecord->ExceptionCode == STATUS_STACK_OVERFLOW || info->ExceptionRecord->ExceptionCode == STATUS_HEAP_CORRUPTION)
{
const char* stack = tf_util_backtrace_string();
tf_printf("ERROR:\n%s\n", stack);
tf_free((void*)stack);
_exit(1);
}
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
static void _startup(int argc, char* argv[]) static void _startup(int argc, char* argv[])
{ {
char buffer[8] = { 0 }; char buffer[8] = { 0 };
@ -1041,20 +1620,24 @@ static void _startup(int argc, char* argv[])
#endif #endif
bool use_error_handler = false; bool use_error_handler = false;
#if defined(__ANDROID__) #if defined(__ANDROID__) || defined(_WIN32)
use_error_handler = true; use_error_handler = true;
#endif #endif
if (use_error_handler) if (use_error_handler)
{ {
if ( if (
#if !defined(_WIN32) #if !defined(_WIN32)
signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSYS, _error_handler) == SIG_ERR ||
#endif #endif
signal(SIGSEGV, _error_handler) == SIG_ERR) signal(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSEGV, _error_handler) == SIG_ERR)
{ {
perror("signal"); perror("signal");
} }
} }
#if defined(_WIN32)
AddVectoredExceptionHandler(0, _win32_exception_handler);
#endif
} }
#if defined(__ANDROID__) #if defined(__ANDROID__)

508
src/ssb.c

File diff suppressed because it is too large Load Diff

View File

@ -103,7 +103,7 @@ static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, v
uint8_t key_bin[k_id_bin_len]; uint8_t key_bin[k_id_bin_len];
if (tf_ssb_id_str_to_bin(key_bin, next->key)) if (tf_ssb_id_str_to_bin(key_bin, next->key))
{ {
tf_ssb_connect(ssb, next->host, next->port, key_bin, 0, NULL, NULL); tf_ssb_connect(ssb, next->host, next->port, key_bin, k_tf_ssb_connect_flag_do_not_store, NULL, NULL);
} }
} }
tf_free(next); tf_free(next);
@ -114,6 +114,7 @@ static void _tf_ssb_connections_timer(uv_timer_t* timer)
tf_ssb_connections_t* connections = timer->data; tf_ssb_connections_t* connections = timer->data;
if (tf_ssb_is_shutting_down(connections->ssb)) if (tf_ssb_is_shutting_down(connections->ssb))
{ {
uv_timer_stop(timer);
return; return;
} }
tf_ssb_connection_t* active[4]; tf_ssb_connection_t* active[4];
@ -182,9 +183,10 @@ static void _tf_ssb_connections_update_work(tf_ssb_t* ssb, void* user_data)
if (sqlite3_bind_text(statement, 1, update->host, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, update->port) == SQLITE_OK && if (sqlite3_bind_text(statement, 1, update->host, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, update->port) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, update->key, -1, NULL) == SQLITE_OK) sqlite3_bind_text(statement, 3, update->key, -1, NULL) == SQLITE_OK)
{ {
if (sqlite3_step(statement) != SQLITE_DONE) int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{ {
tf_printf("tf_ssb_connections_set_attempted: %s.\n", sqlite3_errmsg(db)); tf_printf("tf_ssb_connections_set_attempted: %s aka %s.\n", sqlite3_errstr(r), sqlite3_errmsg(db));
} }
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
@ -197,9 +199,10 @@ static void _tf_ssb_connections_update_work(tf_ssb_t* ssb, void* user_data)
if (sqlite3_bind_text(statement, 1, update->host, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, update->port) == SQLITE_OK && if (sqlite3_bind_text(statement, 1, update->host, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, update->port) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, update->key, -1, NULL) == SQLITE_OK) sqlite3_bind_text(statement, 3, update->key, -1, NULL) == SQLITE_OK)
{ {
if (sqlite3_step(statement) != SQLITE_DONE) int r = sqlite3_step(statement);
if (r != SQLITE_DONE)
{ {
tf_printf("tf_ssb_connections_set_succeeded: %s.\n", sqlite3_errmsg(db)); tf_printf("tf_ssb_connections_set_succeeded: %s aka %s.\n", sqlite3_errstr(r), sqlite3_errmsg(db));
} }
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
@ -215,7 +218,7 @@ static void _tf_ssb_connections_update_work(tf_ssb_t* ssb, void* user_data)
int r = sqlite3_step(statement); int r = sqlite3_step(statement);
if (r != SQLITE_DONE) if (r != SQLITE_DONE)
{ {
tf_printf("tf_ssb_connections_store: %d, %s.\n", r, sqlite3_errmsg(db)); tf_printf("tf_ssb_connections_store: %s aka %s.\n", sqlite3_errstr(r), sqlite3_errmsg(db));
} }
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
@ -231,7 +234,10 @@ static void _tf_ssb_connections_update_after_work(tf_ssb_t* ssb, int status, voi
static void _tf_ssb_connections_queue_update(tf_ssb_connections_t* connections, tf_ssb_connections_update_t* update) static void _tf_ssb_connections_queue_update(tf_ssb_connections_t* connections, tf_ssb_connections_update_t* update)
{ {
tf_ssb_run_work(connections->ssb, _tf_ssb_connections_update_work, _tf_ssb_connections_update_after_work, update); if (!tf_ssb_is_shutting_down(connections->ssb))
{
tf_ssb_run_work(connections->ssb, _tf_ssb_connections_update_work, _tf_ssb_connections_update_after_work, update);
}
} }
void tf_ssb_connections_store(tf_ssb_connections_t* connections, const char* host, int port, const char* key) void tf_ssb_connections_store(tf_ssb_connections_t* connections, const char* host, int port, const char* key)
@ -287,7 +293,7 @@ static void _tf_ssb_connections_sync_broadcast_visit(
} }
else else
{ {
tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot, NULL, NULL); tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot | k_tf_ssb_connect_flag_do_not_store, NULL, NULL);
} }
} }

View File

@ -9,12 +9,14 @@
#include "ow-crypt.h" #include "ow-crypt.h"
#include "sodium/crypto_hash_sha256.h" #include "sodium/crypto_hash_sha256.h"
#include "sodium/crypto_sign.h" #include "sodium/crypto_sign.h"
#include "sodium/crypto_sign_ed25519.h"
#include "sodium/randombytes.h"
#include "sqlite3.h" #include "sqlite3.h"
#include "uv.h" #include "uv.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
typedef struct _message_store_t message_store_t; typedef struct _message_store_t message_store_t;
@ -106,6 +108,23 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
" flags INTEGER," " flags INTEGER,"
" UNIQUE(author, sequence)" " UNIQUE(author, sequence)"
")"); ")");
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_stats')"))
{
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS messages_stats ("
" author TEXT PRIMARY KEY,"
" max_sequence INTEGER,"
" max_timestamp READ"
")");
_tf_ssb_db_exec(db, "INSERT OR REPLACE INTO messages_stats (author, max_sequence, max_timestamp) SELECT author, MAX(sequence), MAX(timestamp) FROM messages GROUP BY author");
}
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ai_stats AFTER INSERT ON messages BEGIN INSERT INTO messages_stats(author, max_sequence, max_timestamp) VALUES (new.author, "
"new.sequence, new.timestamp) ON CONFLICT DO UPDATE SET max_sequence = MAX(max_sequence, excluded.max_sequence), max_timestamp = MAX(max_timestamp, "
"excluded.max_timestamp); END");
_tf_ssb_db_exec(db,
"CREATE TRIGGER IF NOT EXISTS messages_ad_stats AFTER DELETE ON messages BEGIN UPDATE messages_stats SET max_sequence = (SELECT MAX(messages.sequence) FROM messages WHERE "
"messages.author = old.author), max_timestamp = (SELECT MAX(messages.timestamp) FROM messages WHERE messages.author = old.author); END");
if (_tf_ssb_db_has_rows(db, "SELECT name FROM pragma_table_info('messages') WHERE name = 'content' AND type == 'TEXT'")) if (_tf_ssb_db_has_rows(db, "SELECT name FROM pragma_table_info('messages') WHERE name = 'content' AND type == 'TEXT'"))
{ {
@ -168,6 +187,14 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)"); _tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS identities_user ON identities (user, public_key)");
_tf_ssb_db_exec(db, "DELETE FROM identities WHERE user = ':auth'"); _tf_ssb_db_exec(db, "DELETE FROM identities WHERE user = ':auth'");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS invites ("
" invite_public_key TEXT PRIMARY KEY,"
" account TEXT,"
" use_count INTEGER,"
" expires INTEGER"
")");
bool populate_fts = false; bool populate_fts = false;
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')")) if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
{ {
@ -603,11 +630,10 @@ bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_
return result; return result;
} }
bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id) bool tf_ssb_db_blob_has(sqlite3* db, const char* id)
{ {
bool result = false; bool result = false;
sqlite3_stmt* statement; sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1"; const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1";
if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK)
{ {
@ -617,7 +643,6 @@ bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id)
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db);
return result; return result;
} }
@ -979,8 +1004,9 @@ int tf_ssb_sqlite_authorizer(void* user_data, int action_code, const char* arg0,
break; break;
case SQLITE_READ: case SQLITE_READ:
result = (strcmp(arg0, "blob_wants_view") == 0 || strcmp(arg0, "json_each") == 0 || strcmp(arg0, "json_tree") == 0 || strcmp(arg0, "messages") == 0 || result = (strcmp(arg0, "blob_wants_view") == 0 || strcmp(arg0, "json_each") == 0 || strcmp(arg0, "json_tree") == 0 || strcmp(arg0, "messages") == 0 ||
strcmp(arg0, "messages_fts") == 0 || strcmp(arg0, "messages_fts_idx") == 0 || strcmp(arg0, "messages_fts_config") == 0 || strcmp(arg0, "messages_refs") == 0 || strcmp(arg0, "messages_stats") == 0 || strcmp(arg0, "messages_fts") == 0 || strcmp(arg0, "messages_fts_idx") == 0 ||
strcmp(arg0, "messages_refs_message_idx") == 0 || strcmp(arg0, "messages_refs_ref_idx") == 0 || strcmp(arg0, "sqlite_master") == 0 || false) strcmp(arg0, "messages_fts_config") == 0 || strcmp(arg0, "messages_refs") == 0 || strcmp(arg0, "messages_refs_message_idx") == 0 ||
strcmp(arg0, "messages_refs_ref_idx") == 0 || strcmp(arg0, "sqlite_master") == 0 || false)
? SQLITE_OK ? SQLITE_OK
: SQLITE_DENY; : SQLITE_DENY;
break; break;
@ -1306,9 +1332,9 @@ static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blo
return false; return false;
} }
static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks) static following_t* _make_following_node(const char* id, following_t*** following, int* following_count, block_node_t* blocks, bool include_blocks)
{ {
if (_is_blocked_by_active_blocks(id, blocks)) if (!include_blocks && _is_blocked_by_active_blocks(id, blocks))
{ {
return NULL; return NULL;
} }
@ -1335,7 +1361,7 @@ static following_t* _make_following_node(const char* id, following_t*** followin
return entry; return entry;
} }
static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks) static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, block_node_t* active_blocks, bool include_blocks)
{ {
sqlite3* db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL; sqlite3_stmt* statement = NULL;
@ -1354,7 +1380,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
if (sqlite3_column_type(statement, 1) != SQLITE_NULL) if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
{ {
bool is_following = sqlite3_column_int(statement, 1) != 0; bool is_following = sqlite3_column_int(statement, 1) != 0;
following_t* next = _make_following_node(contact, following, following_count, active_blocks); following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks);
if (next) if (next)
{ {
if (is_following) if (is_following)
@ -1376,7 +1402,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
if (sqlite3_column_type(statement, 2) != SQLITE_NULL) if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
{ {
bool is_blocking = sqlite3_column_int(statement, 2) != 0; bool is_blocking = sqlite3_column_int(statement, 2) != 0;
following_t* next = _make_following_node(contact, following, following_count, active_blocks); following_t* next = _make_following_node(contact, following, following_count, active_blocks, include_blocks);
if (next) if (next)
{ {
if (is_blocking) if (is_blocking)
@ -1402,13 +1428,14 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
} }
static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks) static void _get_following(
tf_ssb_t* ssb, following_t* entry, following_t*** following, int* following_count, int depth, int max_depth, block_node_t* active_blocks, bool include_blocks)
{ {
entry->depth = tf_min(depth, entry->depth); entry->depth = tf_min(depth, entry->depth);
if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks)) if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks))
{ {
entry->populated = true; entry->populated = true;
_populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks); _populate_follows_and_blocks(ssb, entry, following, following_count, active_blocks, include_blocks);
if (depth < max_depth) if (depth < max_depth)
{ {
@ -1417,28 +1444,28 @@ static void _get_following(tf_ssb_t* ssb, following_t* entry, following_t*** fol
{ {
if (!_has_following_entry(entry->following[i]->id, entry->blocking, entry->blocking_count)) if (!_has_following_entry(entry->following[i]->id, entry->blocking, entry->blocking_count))
{ {
_get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks); _get_following(ssb, entry->following[i], following, following_count, depth + 1, max_depth, &blocks, include_blocks);
} }
} }
} }
} }
} }
tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth) tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth, bool include_blocks)
{ {
following_t** following = NULL; following_t** following = NULL;
int following_count = 0; int following_count = 0;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL); following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, include_blocks);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL); _get_following(ssb, entry, &following, &following_count, 0, depth, NULL, include_blocks);
entry->ref_count++; entry->ref_count++;
} }
int actual_following_count = 0; int actual_following_count = 0;
for (int i = 0; i < following_count; i++) for (int i = 0; i < following_count; i++)
{ {
if (following[i]->ref_count > 0) if (following[i]->ref_count > 0 || include_blocks)
{ {
actual_following_count++; actual_following_count++;
} }
@ -1450,13 +1477,14 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in
int write_index = 0; int write_index = 0;
for (int i = 0; i < following_count; i++) for (int i = 0; i < following_count; i++)
{ {
if (following[i]->ref_count > 0) if (following[i]->ref_count > 0 || include_blocks)
{ {
snprintf(result[write_index].id, sizeof(result[write_index].id), "%s", following[i]->id); snprintf(result[write_index].id, sizeof(result[write_index].id), "%s", following[i]->id);
result[write_index].following_count = following[i]->following_count; result[write_index].following_count = following[i]->following_count;
result[write_index].blocking_count = following[i]->blocking_count; result[write_index].blocking_count = following[i]->blocking_count;
result[write_index].followed_by_count = following[i]->ref_count; result[write_index].followed_by_count = following[i]->ref_count;
result[write_index].blocked_by_count = following[i]->block_ref_count; result[write_index].blocked_by_count = following[i]->block_ref_count;
result[write_index].depth = following[i]->depth;
write_index++; write_index++;
} }
} }
@ -1478,8 +1506,8 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
int following_count = 0; int following_count = 0;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL); following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, false);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL); _get_following(ssb, entry, &following, &following_count, 0, depth, NULL, false);
entry->ref_count++; entry->ref_count++;
} }
@ -1902,6 +1930,12 @@ static void _tf_ssb_db_resolve_index_work(tf_ssb_t* ssb, void* user_data)
} }
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
if (!request->path)
{
/* From default global settings. */
request->path = tf_strdup("/~core/ssb/");
}
} }
static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -2030,6 +2064,10 @@ bool tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool* out_
{ {
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db)); tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
} }
if (!result)
{
*out_value = tf_util_get_default_global_setting_bool(name);
}
return result; return result;
} }
@ -2053,6 +2091,10 @@ bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t*
{ {
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db)); tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
} }
if (!result)
{
*out_value = tf_util_get_default_global_setting_int(name);
}
return result; return result;
} }
@ -2076,5 +2118,146 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
{ {
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db)); tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
} }
if (!result)
{
snprintf(out_value, size, "%s", tf_util_get_default_global_setting_string(name));
}
return result; return result;
} }
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id)
{
const char* result = NULL;
sqlite3_stmt* statement;
if (sqlite3_prepare(db,
"SELECT json(json_group_object(key, value)) FROM (SELECT fields.key, RANK() OVER (PARTITION BY fields.key ORDER BY messages.sequence DESC) AS rank, fields.value FROM "
"messages, json_each(messages.content) AS fields WHERE messages.author = ? AND messages.content ->> '$.type' = 'about' AND messages.content ->> '$.about' = "
"messages.author AND NOT fields.key IN ('about', 'type')) WHERE rank = 1",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id)
{
const char* result = NULL;
sqlite3_stmt* statement;
if (sqlite3_prepare(db,
"SELECT name FROM (SELECT messages.author, RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, "
"messages.content ->> 'name' AS name FROM messages WHERE messages.author = ? "
"AND json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) "
"WHERE author_rank = 1",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
static void _tf_ssb_db_invite_cleanup(sqlite3* db)
{
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "DELETE FROM invites WHERE use_count = 0 OR expires < ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_int64(statement, 1, (int64_t)time(NULL)) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_DONE)
{
if (sqlite3_changes(db))
{
tf_printf("Cleaned up %d used/expired invites.\n", sqlite3_changes(db));
}
}
else
{
tf_printf("Invite cleanup failed: %s\n", sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
}
bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, int port, int use_count, int expires_seconds, char* out_invite, size_t size)
{
if (use_count < -1 || use_count == 0)
{
return false;
}
uint8_t public_key[crypto_sign_ed25519_PUBLICKEYBYTES] = { 0 };
uint8_t secret_key[crypto_sign_ed25519_SECRETKEYBYTES] = { 0 };
uint8_t seed[crypto_sign_ed25519_SEEDBYTES] = { 0 };
randombytes_buf(seed, sizeof(seed));
crypto_sign_ed25519_seed_keypair(public_key, secret_key, seed);
char public[k_id_base64_len];
tf_ssb_id_bin_to_str(public, sizeof(public), public_key);
char seed_b64[64];
tf_base64_encode(seed, sizeof(seed), seed_b64, sizeof(seed_b64));
bool inserted = false;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "INSERT INTO invites (invite_public_key, account, use_count, expires) VALUES (?, ?, ?, ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, public, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 3, use_count) == SQLITE_OK && sqlite3_bind_int64(statement, 4, (int64_t)time(NULL) + expires_seconds) == SQLITE_OK)
{
inserted = sqlite3_step(statement) == SQLITE_DONE;
}
sqlite3_finalize(statement);
}
_tf_ssb_db_invite_cleanup(db);
snprintf(out_invite, size, "%s:%d:%s~%s", host, port, id, seed_b64);
return inserted;
}
bool tf_ssb_db_use_invite(sqlite3* db, const char* id)
{
bool used = false;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "UPDATE invites SET use_count = use_count - 1 WHERE invite_public_key = ? AND expires >= ? AND (use_count > 0 OR use_count = -1)", -1, &statement,
NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, (int64_t)time(NULL)) == SQLITE_OK)
{
used = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) > 0;
}
sqlite3_finalize(statement);
}
_tf_ssb_db_invite_cleanup(db);
return used;
}

View File

@ -39,11 +39,11 @@ bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_
/** /**
** Determine whether a blob is in the database by ID. ** Determine whether a blob is in the database by ID.
** @param ssb The SSB instasnce. ** @param db The SQLite database instance to use.
** @param id The blob identifier. ** @param id The blob identifier.
** @return true If the blob is in the database. ** @return true If the blob is in the database.
*/ */
bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id); bool tf_ssb_db_blob_has(sqlite3* db, const char* id);
/** /**
** Retrieve a blob from the database. ** Retrieve a blob from the database.
@ -286,6 +286,8 @@ typedef struct _tf_ssb_following_t
int followed_by_count; int followed_by_count;
/** The number of known users blocking the account. */ /** The number of known users blocking the account. */
int blocked_by_count; int blocked_by_count;
/** Degree of separation between initial accounts and this account. */
int depth;
/** The account's identity. */ /** The account's identity. */
char id[k_id_base64_len]; char id[k_id_base64_len];
} tf_ssb_following_t; } tf_ssb_following_t;
@ -306,9 +308,10 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
** @param ids An array of identities. ** @param ids An array of identities.
** @param count The number of identities. ** @param count The number of identities.
** @param depth The following depth to use (prefer 2). ** @param depth The following depth to use (prefer 2).
** @return An array of information about visible accounts. Fere with tf_free(). ** @param include_blocks Whether to include blocked identities in results.
** @return An array of information about visible accounts. Free with tf_free().
*/ */
tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth); tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, int count, int depth, bool include_blocks);
/** /**
** Get all visible identities from all local accounts. ** Get all visible identities from all local accounts.
@ -483,6 +486,55 @@ bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t*
*/ */
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size); bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size);
/**
** Get the latest profile information for the given identity.
** @param db The database.
** @param id The identity.
** @return A JSON representation of the latest profile information set for the account. Free with tf_free().
*/
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id);
/**
** Get the latest profile name for the given identity.
** @param db The database.
** @param id The identity.
** @return The name. Free with tf_free().
*/
const char* tf_ssb_db_get_profile_name(sqlite3* db, const char* id);
/**
** Generate an invite code and store information for it to be usable.
** @param db The database.
** @param id The identity.
** @param host Hostname to which recipient should connect.
** @param port The port to which the recipient should connect.
** @param use_count Number of times the invite code is allowed to be used, or -1 for indefinitely.
** @param expires_seconds How long the invite lasts.
** @param out_invite Populated with the invite code on success.
** @param size The size of the out_invite buffer.
** @return true If an invite was generated.
*/
bool tf_ssb_db_generate_invite(sqlite3* db, const char* id, const char* host, int port, int use_count, int expires_seconds, char* out_invite, size_t size);
/**
** Consume and validate an invite.
** @param db The database.
** @param id The invite public key.
** @return true If the invite was valid and successfully consumed.
*/
bool tf_ssb_db_use_invite(sqlite3* db, const char* id);
/**
** Determine if an account is familiar, meaning it is local or within the given
** follow depth of the local accounts or we have already replicated data for
** it.
** @param db The database.
** @param id The identity.
** @param depth The follow depth.
** @return true if the account is familiar.
*/
bool tf_ssb_db_is_account_familiar(sqlite3* db, const char* id, int depth);
/** /**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer. ** @param user_data User data registered with the authorizer.

322
src/ssb.ebt.c Normal file
View File

@ -0,0 +1,322 @@
#include "ssb.ebt.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "util.js.h"
#include "uv.h"
#include <string.h>
typedef struct _ebt_entry_t
{
char id[k_id_base64_len];
int64_t out;
int64_t in;
bool out_replicate;
bool out_receive;
bool in_replicate;
bool in_receive;
} ebt_entry_t;
typedef struct _tf_ssb_ebt_t
{
tf_ssb_connection_t* connection;
uv_mutex_t mutex;
ebt_entry_t* entries;
int entries_count;
int send_clock_pending;
} tf_ssb_ebt_t;
tf_ssb_ebt_t* tf_ssb_ebt_create(tf_ssb_connection_t* connection)
{
tf_ssb_ebt_t* ebt = tf_malloc(sizeof(tf_ssb_ebt_t));
*ebt = (tf_ssb_ebt_t) {
.connection = connection,
};
uv_mutex_init(&ebt->mutex);
return ebt;
}
void tf_ssb_ebt_destroy(tf_ssb_ebt_t* ebt)
{
uv_mutex_destroy(&ebt->mutex);
tf_free(ebt->entries);
tf_free(ebt);
}
static int _ebt_entry_compare(const void* a, const void* b)
{
const char* id = a;
const ebt_entry_t* entry = b;
return strcmp(id, entry->id);
}
static ebt_entry_t* _ebt_get_entry(tf_ssb_ebt_t* ebt, const char* id)
{
int index = tf_util_insert_index(id, ebt->entries, ebt->entries_count, sizeof(ebt_entry_t), _ebt_entry_compare);
if (index < ebt->entries_count && strcmp(id, ebt->entries[index].id) == 0)
{
return &ebt->entries[index];
}
else
{
ebt->entries = tf_resize_vec(ebt->entries, (ebt->entries_count + 1) * sizeof(ebt_entry_t));
if (index < ebt->entries_count)
{
memmove(ebt->entries + index + 1, ebt->entries + index, (ebt->entries_count - index) * sizeof(ebt_entry_t));
}
ebt->entries[index] = (ebt_entry_t) {
.in = -1,
.out = -1,
};
snprintf(ebt->entries[index].id, sizeof(ebt->entries[index].id), "%s", id);
ebt->entries_count++;
return &ebt->entries[index];
}
}
void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clock)
{
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, clock, JS_GPN_STRING_MASK) == 0)
{
uv_mutex_lock(&ebt->mutex);
for (uint32_t i = 0; i < plen; ++i)
{
JSValue in_clock = JS_UNDEFINED;
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, clock, ptab[i].atom) == 1)
{
in_clock = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
if (!JS_IsUndefined(in_clock))
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
const char* author = JS_ToCString(context, key);
int64_t sequence = -1;
JS_ToInt64(context, &sequence, in_clock);
ebt_entry_t* entry = _ebt_get_entry(ebt, author);
if (sequence < 0)
{
entry->in = -1;
entry->in_replicate = false;
entry->in_receive = false;
}
else
{
entry->in = sequence >> 1;
entry->in_replicate = true;
entry->in_receive = (sequence & 1) == 0;
}
if (!entry->in_receive)
{
tf_ssb_connection_remove_new_message_request(ebt->connection, author);
}
JS_FreeCString(context, author);
JS_FreeValue(context, key);
}
JS_FreeValue(context, in_clock);
}
uv_mutex_unlock(&ebt->mutex);
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
}
}
typedef struct _ebt_get_clock_t
{
tf_ssb_ebt_t* ebt;
int32_t request_number;
tf_ssb_ebt_clock_callback_t* callback;
tf_ssb_ebt_clock_t* clock;
void* user_data;
} ebt_get_clock_t;
static int _ebt_compare_entry(const void* a, const void* b)
{
const char* id = a;
const tf_ssb_ebt_clock_entry_t* entry = b;
return strcmp(id, entry->id);
}
static void _ebt_add_to_clock(ebt_get_clock_t* work, const char* id, int64_t value, bool replicate, bool receive)
{
int count = work->clock ? work->clock->count : 0;
ebt_entry_t* entry = _ebt_get_entry(work->ebt, id);
if ((replicate && !entry->out_replicate) || (receive && !entry->out_receive) || ((replicate || receive || entry->out_replicate || entry->out_receive) && entry->out != value))
{
entry->out = value;
entry->out_replicate = entry->out_replicate || replicate;
entry->out_receive = entry->out_receive || receive;
int index = tf_util_insert_index(id, count ? work->clock->entries : NULL, count, sizeof(tf_ssb_ebt_clock_entry_t), _ebt_compare_entry);
int64_t out_value = entry->out_replicate ? ((value << 1) | (entry->out_receive ? 0 : 1)) : -1;
if (index < count && strcmp(id, work->clock->entries[index].id) == 0)
{
work->clock->entries[index].value = out_value;
}
else
{
work->clock = tf_resize_vec(work->clock, sizeof(tf_ssb_ebt_clock_t) + (count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
if (index < count)
{
memmove(work->clock->entries + index + 1, work->clock->entries + index, (count - index) * sizeof(tf_ssb_ebt_clock_entry_t));
}
work->clock->entries[index] = (tf_ssb_ebt_clock_entry_t) { .value = out_value };
snprintf(work->clock->entries[index].id, sizeof(work->clock->entries[index].id), "%s", id);
work->clock->count = count + 1;
}
}
}
static void _tf_ssb_ebt_get_send_clock_work(tf_ssb_connection_t* connection, void* user_data)
{
ebt_get_clock_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(work->ebt->connection);
int64_t depth = 2;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "replication_hops", &depth);
tf_ssb_release_db_reader(ssb, db);
/* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
if (visible)
{
int64_t* sequences = NULL;
for (int i = 0; visible[i]; i++)
{
int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, visible[i], &sequence, NULL, 0);
sequences = tf_resize_vec(sequences, (i + 1) * sizeof(int64_t));
sequences[i] = sequence;
}
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; visible[i]; i++)
{
_ebt_add_to_clock(work, visible[i], sequences[i], true, true);
}
uv_mutex_unlock(&work->ebt->mutex);
tf_free(visible);
tf_free(sequences);
}
/* Ask about the incoming connection, too. */
char id[k_id_base64_len] = "";
if (tf_ssb_connection_get_id(connection, id, sizeof(id)))
{
int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
uv_mutex_lock(&work->ebt->mutex);
_ebt_add_to_clock(work, id, sequence, true, true);
uv_mutex_unlock(&work->ebt->mutex);
}
/* Also respond with what we know about all requested identities. */
tf_ssb_ebt_clock_entry_t* requested = NULL;
int requested_count = 0;
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; i < work->ebt->entries_count; i++)
{
ebt_entry_t* entry = &work->ebt->entries[i];
if (entry->in_replicate && !entry->out_replicate)
{
requested = tf_resize_vec(requested, (requested_count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
requested[requested_count] = (tf_ssb_ebt_clock_entry_t) { .value = -1 };
snprintf(requested[requested_count].id, sizeof(requested[requested_count].id), "%s", entry->id);
requested_count++;
}
}
uv_mutex_unlock(&work->ebt->mutex);
if (requested_count)
{
for (int i = 0; i < requested_count; i++)
{
tf_ssb_db_get_latest_message_by_author(ssb, requested[i].id, &requested[i].value, NULL, 0);
}
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; i < requested_count; i++)
{
_ebt_add_to_clock(work, requested[i].id, requested[i].value, requested[i].value >= 0, false);
}
uv_mutex_unlock(&work->ebt->mutex);
tf_free(requested);
}
}
static void _tf_ssb_ebt_get_send_clock_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
{
ebt_get_clock_t* work = user_data;
work->callback(work->clock, work->request_number, work->user_data);
tf_free(work->clock);
tf_free(work);
}
void tf_ssb_ebt_get_send_clock(tf_ssb_ebt_t* ebt, int32_t request_number, tf_ssb_ebt_clock_callback_t* callback, void* user_data)
{
ebt_get_clock_t* work = tf_malloc(sizeof(ebt_get_clock_t));
*work = (ebt_get_clock_t) {
.ebt = ebt,
.request_number = request_number,
.callback = callback,
.user_data = user_data,
};
tf_ssb_connection_run_work(ebt->connection, _tf_ssb_ebt_get_send_clock_work, _tf_ssb_ebt_get_send_clock_after_work, work);
}
tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt)
{
int count = 0;
tf_ssb_ebt_clock_t* clock = NULL;
uv_mutex_lock(&ebt->mutex);
for (int i = 0; i < ebt->entries_count; i++)
{
ebt_entry_t* entry = &ebt->entries[i];
if (entry->in_replicate && entry->in_receive && entry->out > entry->in)
{
clock = tf_resize_vec(clock, sizeof(tf_ssb_ebt_clock_t) + (count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
clock->entries[count] = (tf_ssb_ebt_clock_entry_t) { .value = entry->in };
snprintf(clock->entries[count].id, sizeof(clock->entries[count].id), "%s", entry->id);
clock->count = ++count;
}
}
uv_mutex_unlock(&ebt->mutex);
return clock;
}
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence)
{
uv_mutex_lock(&ebt->mutex);
ebt_entry_t* entry = _ebt_get_entry(ebt, id);
entry->in = tf_max(entry->in, sequence);
if (entry->in == entry->out && (tf_ssb_connection_get_flags(ebt->connection) & k_tf_ssb_connect_flag_one_shot) == 0)
{
tf_ssb_connection_add_new_message_request(ebt->connection, id, tf_ssb_connection_get_ebt_request_number(ebt->connection), false);
}
uv_mutex_unlock(&ebt->mutex);
}
int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt)
{
return ebt->send_clock_pending;
}
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending)
{
ebt->send_clock_pending = pending;
}

99
src/ssb.ebt.h Normal file
View File

@ -0,0 +1,99 @@
#pragma once
#include "ssb.h"
#include "quickjs.h"
typedef struct _tf_ssb_connection_t tf_ssb_connection_t;
/**
** SSB EBT state.
*/
typedef struct _tf_ssb_ebt_t tf_ssb_ebt_t;
/**
** An EBT clock entry (identity + sequence pair).
*/
typedef struct _tf_ssb_ebt_clock_entry_t
{
/** The identity. */
char id[k_id_base64_len];
/** The sequence number. */
int64_t value;
} tf_ssb_ebt_clock_entry_t;
/**
** A set of IDs and sequence values.
*/
typedef struct _tf_ssb_ebt_clock_t
{
/** Number of entries. */
int count;
/** Clock entries. */
tf_ssb_ebt_clock_entry_t entries[];
} tf_ssb_ebt_clock_t;
/**
** A callback with EBT clock state.
*/
typedef void(tf_ssb_ebt_clock_callback_t)(const tf_ssb_ebt_clock_t* clock, int32_t request_number, void* user_data);
/**
** Create an EBT instance.
** @param connection The SSB connection to which this EBT state applies.
** @return The EBT instance.
*/
tf_ssb_ebt_t* tf_ssb_ebt_create(tf_ssb_connection_t* connection);
/**
** Update the EBT state with a received clock.
** @param ebt The EBT instance.
** @param context The JS context.
** @param clock The received clock.
*/
void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clock);
/**
** Get the EBT clock state to send.
** @param ebt The EBT instance.
** @param request_number The request number for which the clock will be sent.
** @param callback Called with the clock when determined.
** @param user_data User data passed to the callback.
*/
void tf_ssb_ebt_get_send_clock(tf_ssb_ebt_t* ebt, int32_t request_number, tf_ssb_ebt_clock_callback_t* callback, void* user_data);
/**
** Get the set of messages requested to be sent.
** @param ebt The EBT instance.
** @return A clock of identities and sequence numbers indicating which messages
** are due to be sent. The caller must free with tf_free().
*/
tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt);
/**
** Update the clock state indicating the messages that have been sent for an account.
** @param ebt The EBT instance.
** @param id The identity to update.
** @param sequence The maximum sequence number sent.
*/
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence);
/**
** Destroy an EBT instance.
** @param ebt The EBT instance.
*/
void tf_ssb_ebt_destroy(tf_ssb_ebt_t* ebt);
/**
** Get whether sending the clock is pending.
** @param ebt The EBT instance.
** @return The last value set by tf_ssb_ebt_set_send_clock_pending().
*/
int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt);
/**
** Set whether sending the clock is pending.
** @param ebt The EBT instance.
** @param pending A value representing the pending status.
*/
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending);

View File

@ -30,6 +30,8 @@ enum
k_ssb_blob_bytes_max = 5 * 1024 * 1024, k_ssb_blob_bytes_max = 5 * 1024 * 1024,
k_ssb_peer_exchange_expires_seconds = 60 * 60, k_ssb_peer_exchange_expires_seconds = 60 * 60,
k_max_private_message_recipients = 8,
}; };
/** /**
@ -71,12 +73,16 @@ typedef enum _tf_ssb_message_flags_t
typedef enum _tf_ssb_connect_flags_t typedef enum _tf_ssb_connect_flags_t
{ {
k_tf_ssb_connect_flag_one_shot = 0x1, k_tf_ssb_connect_flag_one_shot = 0x1,
k_tf_ssb_connect_flag_do_not_store = 0x2,
k_tf_ssb_connect_flag_use_invite = 0x4,
} tf_ssb_connect_flags_t; } tf_ssb_connect_flags_t;
/** An SSB instance. */ /** An SSB instance. */
typedef struct _tf_ssb_t tf_ssb_t; typedef struct _tf_ssb_t tf_ssb_t;
/** An SSB connection. */ /** An SSB connection. */
typedef struct _tf_ssb_connection_t tf_ssb_connection_t; typedef struct _tf_ssb_connection_t tf_ssb_connection_t;
/** A connection's EBT state. */
typedef struct _tf_ssb_ebt_t tf_ssb_ebt_t;
/** A trace instance. */ /** A trace instance. */
typedef struct _tf_trace_t tf_trace_t; typedef struct _tf_trace_t tf_trace_t;
/** An SQLite database handle. */ /** An SQLite database handle. */
@ -184,6 +190,13 @@ void tf_ssb_start_periodic(tf_ssb_t* ssb);
*/ */
void tf_ssb_set_verbose(tf_ssb_t* ssb, bool verbose); void tf_ssb_set_verbose(tf_ssb_t* ssb, bool verbose);
/**
** Reduce logging verbosity.
** @param ssb The SSB instance.
** @param quiet Disable unnecessary messages.
*/
void tf_ssb_set_quiet(tf_ssb_t* ssb, bool quiet);
/** /**
** Acquire an SQLite database for unrestricted reading. Release qith tf_ssb_release_db_reader(). ** Acquire an SQLite database for unrestricted reading. Release qith tf_ssb_release_db_reader().
** @param ssb The SSB instance. ** @param ssb The SSB instance.
@ -525,6 +538,13 @@ void tf_ssb_connection_close(tf_ssb_connection_t* connection, const char* reason
*/ */
bool tf_ssb_connection_is_connected(tf_ssb_connection_t* connection); bool tf_ssb_connection_is_connected(tf_ssb_connection_t* connection);
/**
** Check whether a connection is in the process of closing.
** @param connection The connection.
** @return True if the connection is closing.
*/
bool tf_ssb_connection_is_closing(tf_ssb_connection_t* connection);
/** /**
** Get the next outgoing request number for a connection. ** Get the next outgoing request number for a connection.
** @param connection The connection. ** @param connection The connection.
@ -871,6 +891,28 @@ int32_t tf_ssb_connection_get_attendant_request_number(tf_ssb_connection_t* conn
*/ */
void tf_ssb_connection_set_attendant(tf_ssb_connection_t* connection, bool attendant, int request_number); void tf_ssb_connection_set_attendant(tf_ssb_connection_t* connection, bool attendant, int request_number);
/**
** Register for endpoint change notifications on a connection.
** @param connection The SHS connection.
** @param endpoint Whether this connection will be an endpoint.
** @param request_number The request number on which to send endpoint changes.
*/
void tf_ssb_connection_set_endpoint(tf_ssb_connection_t* connection, bool endpoint, int request_number);
/**
** Get whether we are a potential tunnel endpoint.
** @param connection The SHS connection.
** @return True if this is an endpoint connection.
*/
bool tf_ssb_connection_is_endpoint(tf_ssb_connection_t* connection);
/**
** Get the request number used to notify of tunnel endpoint changes.
** @param connection the SHS connection.
** @return A request number.
*/
int32_t tf_ssb_connection_get_endpoint_request_number(tf_ssb_connection_t* connection);
/** /**
** Clear all attendants from a room. ** Clear all attendants from a room.
** @param connection The SHS connection. ** @param connection The SHS connection.
@ -916,20 +958,6 @@ int32_t tf_ssb_connection_get_ebt_request_number(tf_ssb_connection_t* connection
*/ */
void tf_ssb_connection_set_ebt_request_number(tf_ssb_connection_t* connection, int32_t request_number); void tf_ssb_connection_set_ebt_request_number(tf_ssb_connection_t* connection, int32_t request_number);
/**
** Get whether the EBT clock has been sent for a connection.
** @param connection An SHS connection.
** @return True if the clock has been sent.
*/
bool tf_ssb_connection_get_sent_clock(tf_ssb_connection_t* connection);
/**
** Set the EBT clock sent state for a connection.
** @param connection An SHS connection.
** @param sent_clock Whether the clock has been sent.
*/
void tf_ssb_connection_set_sent_clock(tf_ssb_connection_t* connection, bool sent_clock);
/** /**
** Get the JS class ID of the SSB connection class. ** Get the JS class ID of the SSB connection class.
** @return The class ID ** @return The class ID
@ -1125,4 +1153,22 @@ void tf_ssb_sync_start(tf_ssb_t* ssb);
*/ */
int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection); int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection);
/**
** Get a connection's EBT state.
** @param connection The connection.
** @return the EBT state for the connection.
*/
tf_ssb_ebt_t* tf_ssb_connection_get_ebt(tf_ssb_connection_t* connection);
/**
** Encrypt a private message to a set of recipients.
** @param private_key The private key of the author.
** @param recipients A list of recipient identities.
** @param recipients_count The number of recipients in recipients.
** @param message The plain text to post.
** @param message_size The length in bytes of message.
** @return A secret box string. Free with tf_free().
*/
char* tf_ssb_private_message_encrypt(uint8_t* private_key, const char** recipients, int recipients_count, const char* message, size_t message_size);
/** @} */ /** @} */

View File

@ -251,111 +251,6 @@ static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val,
return result; return result;
} }
static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_val, const char* id, bool follow)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JSValue server_user = JS_NewString(context, ":admin");
char server_id_buffer[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, server_id_buffer, sizeof(server_id_buffer));
JSValue server_id = JS_NewString(context, server_id_buffer);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact"));
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, id));
JS_SetPropertyStr(context, message, "following", JS_NewBool(context, follow));
JSValue args[] = {
server_user,
server_id,
message,
};
JSValue result = _tf_ssb_appendMessageWithIdentity(context, this_val, tf_countof(args), args);
JS_FreeValue(context, server_id);
JS_FreeValue(context, server_user);
JS_FreeValue(context, message);
return result;
}
typedef struct _set_server_following_me_t
{
const char* user;
const char* key;
bool follow;
JSValue this_val;
JSValue promise[2];
bool error_does_not_own_key;
bool append_message;
} set_server_following_me_t;
static void _tf_ssb_set_server_following_me_work(tf_ssb_t* ssb, void* user_data)
{
set_server_following_me_t* work = user_data;
if (!tf_ssb_db_identity_get_private_key(ssb, work->user, work->key, NULL, 0))
{
work->error_does_not_own_key = true;
}
else
{
char server_id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, server_id, sizeof(server_id));
const char* server_id_ptr = server_id;
const char** current_following = tf_ssb_db_following_deep_ids(ssb, &server_id_ptr, 1, 1);
bool is_following = false;
for (const char** it = current_following; *it; it++)
{
if (strcmp(work->key, *it) == 0)
{
is_following = true;
break;
}
}
tf_free(current_following);
work->append_message = (work->follow && !is_following) || (!work->follow && is_following);
}
}
static void _tf_ssb_set_server_following_me_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
set_server_following_me_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->error_does_not_own_key)
{
result = JS_ThrowInternalError(context, "User %s does not own key %s.", work->user, work->key);
}
else if (work->append_message)
{
result = _set_server_following_internal(ssb, work->this_val, work->key, work->follow);
}
JS_FreeCString(context, work->key);
JS_FreeCString(context, work->user);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->this_val);
tf_free(work);
}
static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
set_server_following_me_t* work = tf_malloc(sizeof(set_server_following_me_t));
*work = (set_server_following_me_t) {
.user = JS_ToCString(context, argv[0]),
.key = JS_ToCString(context, argv[1]),
.follow = JS_ToBool(context, argv[2]),
.this_val = JS_DupValue(context, this_val),
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_set_server_following_me_work, _tf_ssb_set_server_following_me_after_work, work);
}
return result;
}
typedef struct _swap_with_server_identity_t typedef struct _swap_with_server_identity_t
{ {
char server_id[k_id_base64_len]; char server_id[k_id_base64_len];
@ -1120,6 +1015,7 @@ static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, in
int flags = tf_ssb_connection_get_flags(connection); int flags = tf_ssb_connection_get_flags(connection);
JS_SetPropertyStr(context, flags_object, "one_shot", JS_NewBool(context, (flags & k_tf_ssb_connect_flag_one_shot) != 0)); JS_SetPropertyStr(context, flags_object, "one_shot", JS_NewBool(context, (flags & k_tf_ssb_connect_flag_one_shot) != 0));
JS_SetPropertyStr(context, object, "flags", flags_object); JS_SetPropertyStr(context, object, "flags", flags_object);
JS_SetPropertyStr(context, object, "connected", JS_NewBool(context, tf_ssb_connection_is_connected(connection)));
const char* destroy_reason = tf_ssb_connection_get_destroy_reason(connection); const char* destroy_reason = tf_ssb_connection_get_destroy_reason(connection);
if (destroy_reason) if (destroy_reason)
{ {
@ -1198,7 +1094,7 @@ static JSValue _tf_ssb_closeConnection(JSContext* context, JSValueConst this_val
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, id); tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, id);
if (connection) if (connection)
{ {
tf_ssb_connection_close(connection, "Close requested by user."); tf_ssb_connection_close(connection, "Closed by user");
} }
JS_FreeCString(context, id); JS_FreeCString(context, id);
return connection ? JS_TRUE : JS_FALSE; return connection ? JS_TRUE : JS_FALSE;
@ -1495,10 +1391,10 @@ static JSValue _tf_ssb_sqlAsync(JSContext* context, JSValueConst this_val, int a
uv_mutex_init(&work->lock); uv_mutex_init(&work->lock);
uv_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_sqlAsync_start_timer); uv_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_sqlAsync_start_timer);
uv_timer_init(tf_ssb_get_loop(ssb), &work->timeout); uv_timer_init(tf_ssb_get_loop(ssb), &work->timeout);
JSValue result = JS_NewPromiseCapability(context, work->promise); JSValue result = JS_UNDEFINED;
JSValue error_value = JS_UNDEFINED;
if (ssb) if (ssb)
{ {
result = JS_NewPromiseCapability(context, work->promise);
int32_t length = tf_util_get_length(context, argv[1]); int32_t length = tf_util_get_length(context, argv[1]);
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
@ -1541,18 +1437,6 @@ static JSValue _tf_ssb_sqlAsync(JSContext* context, JSValueConst this_val, int a
} }
tf_ssb_run_work(ssb, _tf_ssb_sqlAsync_work, _tf_ssb_sqlAsync_after_work, work); tf_ssb_run_work(ssb, _tf_ssb_sqlAsync_work, _tf_ssb_sqlAsync_after_work, work);
} }
if (!JS_IsUndefined(error_value))
{
JSValue call_result = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &error_value);
tf_util_report_error(context, call_result);
JS_FreeValue(context, call_result);
JS_FreeValue(context, error_value);
JS_FreeCString(context, query);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->callback);
_tf_ssb_sqlAsync_destroy(work);
}
return result; return result;
} }
@ -1974,11 +1858,6 @@ static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, i
return result ? JS_TRUE : JS_FALSE; return result ? JS_TRUE : JS_FALSE;
} }
enum
{
k_max_private_message_recipients = 8
};
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]) static bool _tf_ssb_get_private_key_curve25519_internal(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{ {
if (!user || !identity) if (!user || !identity)
@ -2027,14 +1906,12 @@ typedef struct _private_message_encrypt_t
{ {
const char* signer_user; const char* signer_user;
const char* signer_identity; const char* signer_identity;
uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES]; const char* recipients[k_max_private_message_recipients];
int recipient_count; int recipient_count;
const char* message; const char* message;
size_t message_size; size_t message_size;
JSValue promise[2]; JSValue promise[2];
bool error_id_not_found; bool error_id_not_found;
bool error_secretbox_failed;
bool error_scalarmult_failed;
char* encrypted; char* encrypted;
size_t encrypted_length; size_t encrypted_length;
} private_message_encrypt_t; } private_message_encrypt_t;
@ -2050,73 +1927,8 @@ static void _tf_ssb_private_message_encrypt_work(tf_ssb_t* ssb, void* user_data)
if (found) if (found)
{ {
uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 }; work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size);
uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 0 }; work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0;
uint8_t nonce[crypto_box_NONCEBYTES] = { 0 };
uint8_t body_key[crypto_box_SECRETKEYBYTES] = { 0 };
crypto_box_keypair(public_key, secret_key);
randombytes_buf(nonce, sizeof(nonce));
randombytes_buf(body_key, sizeof(body_key));
uint8_t length_and_key[1 + sizeof(body_key)];
length_and_key[0] = (uint8_t)work->recipient_count;
memcpy(length_and_key + 1, body_key, sizeof(body_key));
size_t payload_size =
sizeof(nonce) + sizeof(public_key) + (crypto_secretbox_MACBYTES + sizeof(length_and_key)) * work->recipient_count + crypto_secretbox_MACBYTES + work->message_size;
uint8_t* payload = tf_malloc(payload_size);
uint8_t* p = payload;
memcpy(p, nonce, sizeof(nonce));
p += sizeof(nonce);
memcpy(p, public_key, sizeof(public_key));
p += sizeof(public_key);
for (int i = 0; i < work->recipient_count; i++)
{
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, secret_key, work->recipients[i]) == 0)
{
if (crypto_secretbox_easy(p, length_and_key, sizeof(length_and_key), nonce, shared_secret) != 0)
{
work->error_secretbox_failed = true;
break;
}
else
{
p += crypto_secretbox_MACBYTES + sizeof(length_and_key);
}
}
else
{
work->error_scalarmult_failed = true;
break;
}
}
if (!work->error_secretbox_failed && !work->error_scalarmult_failed)
{
if (crypto_secretbox_easy(p, (const uint8_t*)work->message, work->message_size, nonce, body_key) != 0)
{
work->error_scalarmult_failed = true;
}
else
{
p += crypto_secretbox_MACBYTES + work->message_size;
assert((size_t)(p - payload) == payload_size);
char* encoded = tf_malloc(payload_size * 2 + 5);
size_t encoded_length = tf_base64_encode(payload, payload_size, encoded, payload_size * 2 + 5);
memcpy(encoded + encoded_length, ".box", 5);
encoded_length += 4;
work->encrypted = encoded;
work->encrypted_length = encoded_length;
}
}
tf_free(payload);
} }
else else
{ {
@ -2129,13 +1941,9 @@ static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status
private_message_encrypt_t* work = user_data; private_message_encrypt_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
if (work->error_secretbox_failed) if (!work->encrypted)
{ {
result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed"); result = JS_ThrowInternalError(context, "Encrypt failed.");
}
else if (work->error_scalarmult_failed)
{
result = JS_ThrowInternalError(context, "crypto_scalarmult failed");
} }
else if (work->error_id_not_found) else if (work->error_id_not_found)
{ {
@ -2147,6 +1955,10 @@ static void _tf_ssb_private_message_encrypt_after_work(tf_ssb_t* ssb, int status
tf_free((void*)work->encrypted); tf_free((void*)work->encrypted);
} }
for (int i = 0; i < work->recipient_count; i++)
{
tf_free((void*)work->recipients[i]);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result); JS_FreeValue(context, result);
tf_util_report_error(context, error); tf_util_report_error(context, error);
@ -2168,24 +1980,14 @@ static JSValue _tf_ssb_private_message_encrypt(JSContext* context, JSValueConst
return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients); return JS_ThrowRangeError(context, "Number of recipients must be between 1 and %d.", k_max_private_message_recipients);
} }
uint8_t recipients[k_max_private_message_recipients][crypto_scalarmult_curve25519_SCALARBYTES] = { 0 }; char* recipients[k_max_private_message_recipients] = { 0 };
for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++) for (int i = 0; i < recipient_count && JS_IsUndefined(result); i++)
{ {
JSValue recipient = JS_GetPropertyUint32(context, argv[2], i); JSValue recipient = JS_GetPropertyUint32(context, argv[2], i);
const char* id = JS_ToCString(context, recipient); const char* id = JS_ToCString(context, recipient);
if (id) if (id)
{ {
const char* type = strstr(id, ".ed25519"); recipients[i] = tf_strdup(id);
const char* id_start = *id == '@' ? id + 1 : id;
uint8_t key[crypto_box_PUBLICKEYBYTES] = { 0 };
if (tf_base64_decode(id_start, type ? (size_t)(type - id_start) : strlen(id_start), key, sizeof(key)) != sizeof(key))
{
result = JS_ThrowInternalError(context, "Invalid recipient: %s.\n", id);
}
else if (crypto_sign_ed25519_pk_to_curve25519(recipients[i], key) != 0)
{
result = JS_ThrowInternalError(context, "Failed to convert recipient ID.\n");
}
JS_FreeCString(context, id); JS_FreeCString(context, id);
} }
JS_FreeValue(context, recipient); JS_FreeValue(context, recipient);
@ -2368,7 +2170,7 @@ typedef struct _following_t
static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data)
{ {
following_t* following = user_data; following_t* following = user_data;
following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth); following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth, false);
} }
static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -2385,6 +2187,7 @@ static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_d
JS_SetPropertyStr(context, entry, "ob", JS_NewInt32(context, following->out_following[i].blocking_count)); JS_SetPropertyStr(context, entry, "ob", JS_NewInt32(context, following->out_following[i].blocking_count));
JS_SetPropertyStr(context, entry, "if", JS_NewInt32(context, following->out_following[i].followed_by_count)); JS_SetPropertyStr(context, entry, "if", JS_NewInt32(context, following->out_following[i].followed_by_count));
JS_SetPropertyStr(context, entry, "ib", JS_NewInt32(context, following->out_following[i].blocked_by_count)); JS_SetPropertyStr(context, entry, "ib", JS_NewInt32(context, following->out_following[i].blocked_by_count));
JS_SetPropertyStr(context, entry, "d", JS_NewInt32(context, following->out_following[i].depth));
JS_SetPropertyStr(context, object, following->out_following[i].id, entry); JS_SetPropertyStr(context, object, following->out_following[i].id, entry);
} }
JSValue result = JS_Call(context, following->promise[0], JS_UNDEFINED, 1, &object); JSValue result = JS_Call(context, following->promise[0], JS_UNDEFINED, 1, &object);
@ -2574,7 +2377,6 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1)); JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2)); JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2)); JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "setServerFollowingMe", JS_NewCFunction(context, _tf_ssb_set_server_following_me, "setServerFollowingMe", 3));
JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2)); JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2));
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1)); JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2)); JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));

View File

@ -3,6 +3,7 @@
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "ssb.db.h" #include "ssb.db.h"
#include "ssb.ebt.h"
#include "ssb.h" #include "ssb.h"
#include "util.js.h" #include "util.js.h"
@ -19,30 +20,7 @@ static void _tf_ssb_connection_send_history_stream(
static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection); static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection);
static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms); static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms);
static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms); static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms);
static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data);
static bool _get_global_setting_bool(tf_ssb_t* ssb, const char* name, bool default_value)
{
bool result = default_value;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
result = sqlite3_column_int(statement, 0) != 0;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
static void _tf_ssb_rpc_gossip_ping_callback( static void _tf_ssb_rpc_gossip_ping_callback(
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_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
@ -161,7 +139,9 @@ static void _tf_ssb_rpc_blobs_has_work(tf_ssb_connection_t* connection, void* us
{ {
blobs_has_work_t* work = user_data; blobs_has_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
work->found = tf_ssb_db_blob_has(ssb, work->id); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
work->found = tf_ssb_db_blob_has(db, work->id);
tf_ssb_release_db_reader(ssb, db);
} }
static void _tf_ssb_rpc_blobs_has_after_work(tf_ssb_connection_t* connection, int status, void* user_data) static void _tf_ssb_rpc_blobs_has_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
@ -315,8 +295,28 @@ static void _tf_ssb_rpc_tunnel_callback(tf_ssb_connection_t* connection, uint8_t
if (flags & k_ssb_rpc_flag_end_error) if (flags & k_ssb_rpc_flag_end_error)
{ {
tf_ssb_connection_remove_request(connection, request_number); tf_ssb_connection_remove_request(connection, request_number);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message_val = JS_GetPropertyStr(context, args, "message");
JSValue stack_val = JS_GetPropertyStr(context, args, "stack");
char buffer[1024]; char buffer[1024];
snprintf(buffer, sizeof(buffer), "error from tunnel: %.*s", (int)size, message); if (!JS_IsUndefined(message_val))
{
const char* message_string = JS_ToCString(context, message_val);
const char* stack_string = JS_ToCString(context, stack_val);
snprintf(buffer, sizeof(buffer), "Error from tunnel: %s\n%s", message_string, stack_string);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
}
else
{
snprintf(buffer, sizeof(buffer), "Error from tunnel: %.*s", (int)size, message);
}
JS_FreeValue(context, stack_val);
JS_FreeValue(context, message_val);
tf_ssb_connection_close(tun->connection, buffer); tf_ssb_connection_close(tun->connection, buffer);
} }
else else
@ -435,6 +435,35 @@ static void _tf_ssb_rpc_room_meta(tf_ssb_connection_t* connection, uint8_t flags
JS_FreeValue(context, response); JS_FreeValue(context, response);
} }
static void _tf_ssb_rpc_send_endpoints(tf_ssb_t* ssb)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue endpoints = JS_NewArray(context);
tf_ssb_connection_t* connections[1024];
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
int id_count = 0;
for (int i = 0; i < count; i++)
{
char id[k_id_base64_len] = { 0 };
if ((tf_ssb_connection_is_attendant(connections[i]) || tf_ssb_connection_is_endpoint(connections[i])) && tf_ssb_connection_is_connected(connections[i]) &&
tf_ssb_connection_get_id(connections[i], id, sizeof(id)))
{
JS_SetPropertyUint32(context, endpoints, id_count++, JS_NewString(context, id));
}
}
for (int i = 0; i < count; i++)
{
if (tf_ssb_connection_is_endpoint(connections[i]) && tf_ssb_connection_is_connected(connections[i]))
{
int32_t request_number = tf_ssb_connection_get_ebt_request_number(connections[i]);
tf_ssb_connection_rpc_send_json(connections[i], k_ssb_rpc_flag_json | k_ssb_rpc_flag_stream, -request_number, NULL, endpoints, NULL, NULL, NULL);
}
}
JS_FreeValue(context, endpoints);
}
static void _tf_ssb_rpc_room_attendants(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_room_attendants(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{ {
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
@ -460,6 +489,7 @@ static void _tf_ssb_rpc_room_attendants(tf_ssb_connection_t* connection, uint8_t
tf_ssb_connection_t* connections[1024]; tf_ssb_connection_t* connections[1024];
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections)); int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
bool have_endpoints = false;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
char id[k_id_base64_len] = { 0 }; char id[k_id_base64_len] = { 0 };
@ -469,15 +499,38 @@ static void _tf_ssb_rpc_room_attendants(tf_ssb_connection_t* connection, uint8_t
tf_ssb_connection_rpc_send_json(connections[i], flags, -tf_ssb_connection_get_attendant_request_number(connections[i]), NULL, joined, NULL, NULL, NULL); tf_ssb_connection_rpc_send_json(connections[i], flags, -tf_ssb_connection_get_attendant_request_number(connections[i]), NULL, joined, NULL, NULL, NULL);
} }
if (tf_ssb_connection_is_endpoint(connections[i]))
{
have_endpoints = true;
}
} }
JS_SetPropertyStr(context, state, "ids", ids); JS_SetPropertyStr(context, state, "ids", ids);
tf_ssb_connection_rpc_send_json(connection, flags, -request_number, NULL, state, NULL, NULL, NULL); tf_ssb_connection_rpc_send_json(connection, flags, -request_number, NULL, state, NULL, NULL, NULL);
JS_FreeValue(context, joined); JS_FreeValue(context, joined);
JS_FreeValue(context, state); JS_FreeValue(context, state);
if (have_endpoints)
{
_tf_ssb_rpc_send_endpoints(ssb);
}
tf_ssb_connection_set_attendant(connection, true, request_number); tf_ssb_connection_set_attendant(connection, true, request_number);
} }
static void _tf_ssb_rpc_tunnel_endpoints(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);
if (!tf_ssb_is_room(ssb))
{
tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, "tunnel.endpoints");
return;
}
tf_ssb_connection_add_request(connection, -request_number, "tunnel.endpoints", NULL, NULL, NULL, NULL);
tf_ssb_connection_set_endpoint(connection, true, request_number);
_tf_ssb_rpc_send_endpoints(ssb);
}
typedef struct _blobs_get_t typedef struct _blobs_get_t
{ {
char id[k_blob_id_len]; char id[k_blob_id_len];
@ -879,6 +932,7 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
break; break;
} }
} }
tf_ssb_ebt_set_messages_sent(tf_ssb_connection_get_ebt(connection), request->author, request->out_max_sequence_seen);
if (!request->out_finished) if (!request->out_finished)
{ {
_tf_ssb_connection_send_history_stream( _tf_ssb_connection_send_history_stream(
@ -908,7 +962,7 @@ static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t*
static void _tf_ssb_connection_send_history_stream( static void _tf_ssb_connection_send_history_stream(
tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live, bool end_request) tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live, bool end_request)
{ {
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection))) if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{ {
tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t)); tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t));
*async = (tf_ssb_connection_send_history_stream_t) { *async = (tf_ssb_connection_send_history_stream_t) {
@ -967,197 +1021,20 @@ static void _tf_ssb_rpc_createHistoryStream(
JS_FreeValue(context, arg_array); JS_FreeValue(context, arg_array);
} }
typedef struct _ebt_clock_row_t static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connection)
{ {
char id[k_id_base64_len]; tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
int64_t value; tf_ssb_ebt_clock_t* clock = tf_ssb_ebt_get_messages_to_send(ebt);
} ebt_clock_row_t; if (clock)
typedef struct _ebt_replicate_send_clock_t
{
int64_t request_number;
ebt_clock_row_t* clock;
int clock_count;
char* out_clock;
} ebt_replicate_send_clock_t;
static void _tf_ssb_rpc_ebt_replicate_send_clock_work(tf_ssb_connection_t* connection, void* user_data)
{
ebt_replicate_send_clock_t* work = user_data;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSValue full_clock = JS_NewObject(context);
int64_t depth = 2;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "replication_hops", &depth);
tf_ssb_release_db_reader(ssb, db);
/* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
for (int i = 0; visible[i]; i++)
{ {
int64_t sequence = 0; for (int i = 0; i < clock->count; i++)
tf_ssb_db_get_latest_message_by_author(ssb, visible[i], &sequence, NULL, 0);
JS_SetPropertyStr(context, full_clock, visible[i], JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
}
tf_free(visible);
/* Ask about the incoming connection, too. */
char id[k_id_base64_len] = "";
if (tf_ssb_connection_get_id(connection, id, sizeof(id)))
{
JSValue in_clock = JS_GetPropertyStr(context, full_clock, id);
if (JS_IsUndefined(in_clock))
{ {
int64_t sequence = 0; tf_ssb_ebt_clock_entry_t* entry = &clock->entries[i];
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0); int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection);
JS_SetPropertyStr(context, full_clock, id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1))); bool live = (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_one_shot) == 0;
_tf_ssb_connection_send_history_stream(connection, request_number, entry->id, entry->value, false, live, false);
} }
JS_FreeValue(context, in_clock); tf_free(clock);
}
/* Also respond with what we know about all requested identities. */
for (int i = 0; i < work->clock_count; i++)
{
JSValue in_clock = JS_GetPropertyStr(context, full_clock, work->clock[i].id);
if (JS_IsUndefined(in_clock))
{
int64_t sequence = -1;
tf_ssb_db_get_latest_message_by_author(ssb, work->clock[i].id, &sequence, NULL, 0);
JS_SetPropertyStr(context, full_clock, work->clock[i].id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
}
JS_FreeValue(context, in_clock);
}
JSValue json = JS_JSONStringify(context, full_clock, JS_NULL, JS_NULL);
size_t size = 0;
const char* string = JS_ToCStringLen(context, &size, json);
char* copy = tf_malloc(size + 1);
memcpy(copy, string, size + 1);
work->out_clock = copy;
JS_FreeCString(context, string);
JS_FreeValue(context, json);
JS_FreeValue(context, full_clock);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _tf_ssb_rpc_ebt_replicate_send_clock_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{
ebt_replicate_send_clock_t* work = user_data;
tf_free(work->clock);
if (work->out_clock)
{
tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, -work->request_number, NULL, (const uint8_t*)work->out_clock, strlen(work->out_clock), NULL, NULL, NULL);
tf_free(work->out_clock);
}
tf_free(work);
}
static void _tf_ssb_rpc_ebt_replicate_send_clock(tf_ssb_connection_t* connection, int32_t request_number, JSValue message)
{
ebt_replicate_send_clock_t* work = tf_malloc(sizeof(ebt_replicate_send_clock_t));
*work = (ebt_replicate_send_clock_t) {
.request_number = request_number,
};
JSContext* context = tf_ssb_connection_get_context(connection);
if (!JS_IsUndefined(message))
{
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, message, JS_GPN_STRING_MASK) == 0)
{
work->clock_count = (int)plen;
work->clock = tf_malloc(sizeof(ebt_clock_row_t) * plen);
memset(work->clock, 0, sizeof(ebt_clock_row_t) * plen);
for (uint32_t i = 0; i < plen; ++i)
{
const char* id = JS_AtomToCString(context, ptab[i].atom);
snprintf(work->clock[i].id, sizeof(work->clock[i].id), "%s", id);
JS_FreeCString(context, id);
JSPropertyDescriptor desc = { 0 };
JSValue key_value = JS_UNDEFINED;
if (JS_GetOwnProperty(context, &desc, message, ptab[i].atom) == 1)
{
key_value = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
JS_ToInt64(context, &work->clock[i].value, key_value);
JS_FreeValue(context, key_value);
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
}
}
tf_ssb_connection_run_work(connection, _tf_ssb_rpc_ebt_replicate_send_clock_work, _tf_ssb_rpc_ebt_replicate_send_clock_after_work, work);
}
static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connection, JSValue message)
{
if (JS_IsUndefined(message))
{
return;
}
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, message, JS_GPN_STRING_MASK) == 0)
{
for (uint32_t i = 0; i < plen; ++i)
{
JSValue in_clock = JS_UNDEFINED;
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, message, ptab[i].atom) == 1)
{
in_clock = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
if (!JS_IsUndefined(in_clock))
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
int64_t sequence = -1;
JS_ToInt64(context, &sequence, in_clock);
const char* author = JS_ToCString(context, key);
if (sequence >= 0 && (sequence & 1) == 0)
{
int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection);
bool live = (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_one_shot) == 0;
_tf_ssb_connection_send_history_stream(connection, request_number, author, sequence >> 1, false, live, false);
if (live)
{
tf_ssb_connection_add_new_message_request(connection, author, request_number, false);
}
}
else
{
tf_ssb_connection_remove_new_message_request(connection, author);
}
JS_FreeCString(context, author);
JS_FreeValue(context, key);
}
JS_FreeValue(context, in_clock);
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
} }
} }
@ -1171,17 +1048,54 @@ typedef struct _resend_clock_t
{ {
tf_ssb_connection_t* connection; tf_ssb_connection_t* connection;
int32_t request_number; int32_t request_number;
int pending;
} resend_clock_t; } resend_clock_t;
static void _tf_ssb_rpc_ebt_send_clock_callback(const tf_ssb_ebt_clock_t* clock, int32_t request_number, void* user_data)
{
resend_clock_t* resend = user_data;
tf_ssb_connection_t* connection = resend->connection;
if (clock && clock->count && tf_ssb_connection_is_connected(connection) && !tf_ssb_connection_is_closing(connection))
{
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message = JS_NewObject(context);
for (int i = 0; i < clock->count; i++)
{
JS_SetPropertyStr(context, message, clock->entries[i].id, JS_NewInt64(context, clock->entries[i].value));
}
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, -request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
if (resend->pending != tf_ssb_ebt_get_send_clock_pending(ebt) && tf_ssb_connection_is_connected(connection) &&
!tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
resend->pending = tf_ssb_ebt_get_send_clock_pending(ebt);
tf_ssb_connection_schedule_idle(connection, "ebt.clock", _tf_ssb_rpc_ebt_replicate_resend_clock, resend);
}
else
{
tf_ssb_ebt_set_send_clock_pending(ebt, 0);
tf_free(resend);
}
_tf_ssb_rpc_ebt_replicate_send_messages(connection);
}
static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data) static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data)
{ {
resend_clock_t* resend = user_data; resend_clock_t* resend = user_data;
if (!skip) if (!skip)
{ {
_tf_ssb_rpc_ebt_replicate_send_clock(resend->connection, resend->request_number, JS_UNDEFINED); tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
tf_ssb_connection_set_sent_clock(resend->connection, true); tf_ssb_ebt_get_send_clock(ebt, resend->request_number, _tf_ssb_rpc_ebt_send_clock_callback, resend);
}
else
{
tf_free(resend);
} }
tf_free(user_data);
} }
static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
@ -1204,33 +1118,38 @@ static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t f
JSValue name = JS_GetPropertyStr(context, args, "name"); JSValue name = JS_GetPropertyStr(context, args, "name");
JSValue in_clock = JS_IsUndefined(name) ? args : JS_UNDEFINED; JSValue in_clock = JS_IsUndefined(name) ? args : JS_UNDEFINED;
bool resend_clock = false;
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
if (!JS_IsUndefined(author)) if (!JS_IsUndefined(author))
{ {
/* Looks like a message. */ /* Looks like a message. */
tf_ssb_connection_adjust_read_backpressure(connection, 1); tf_ssb_connection_adjust_read_backpressure(connection, 1);
tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection); tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection);
if (tf_ssb_connection_get_sent_clock(connection) && !tf_ssb_is_shutting_down(ssb)) resend_clock = !tf_ssb_is_shutting_down(ssb) && !tf_ssb_connection_is_closing(connection);
}
else
{
tf_ssb_ebt_receive_clock(ebt, context, in_clock);
resend_clock = true;
}
if (resend_clock && tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
int pending = tf_ssb_ebt_get_send_clock_pending(ebt) + 1;
tf_ssb_ebt_set_send_clock_pending(ebt, pending);
if (pending == 1)
{ {
tf_ssb_connection_set_sent_clock(connection, false);
resend_clock_t* resend = tf_malloc(sizeof(resend_clock_t)); resend_clock_t* resend = tf_malloc(sizeof(resend_clock_t));
*resend = (resend_clock_t) { *resend = (resend_clock_t) {
.connection = connection, .connection = connection,
.request_number = request_number, .request_number = request_number,
.pending = pending,
}; };
tf_ssb_connection_schedule_idle(connection, "ebt.clock", _tf_ssb_rpc_ebt_replicate_resend_clock, resend); tf_ssb_connection_schedule_idle(connection, "ebt.clock", _tf_ssb_rpc_ebt_replicate_resend_clock, resend);
} }
} }
else
{
/* EBT clock. */
if (!tf_ssb_connection_get_sent_clock(connection))
{
_tf_ssb_rpc_ebt_replicate_send_clock(connection, request_number, in_clock);
tf_ssb_connection_set_sent_clock(connection, true);
}
_tf_ssb_rpc_ebt_replicate_send_messages(connection, in_clock);
}
JS_FreeValue(context, name); JS_FreeValue(context, name);
JS_FreeValue(context, author); JS_FreeValue(context, author);
} }
@ -1284,6 +1203,122 @@ static void _tf_ssb_rpc_ebt_replicate_server(
tf_ssb_connection_add_request(connection, -request_number, "ebt.replicate", _tf_ssb_rpc_ebt_replicate, NULL, NULL, NULL); tf_ssb_connection_add_request(connection, -request_number, "ebt.replicate", _tf_ssb_rpc_ebt_replicate, NULL, NULL, NULL);
} }
typedef struct _invite_use_t
{
tf_ssb_t* ssb;
char author[k_id_base64_len];
;
uint8_t private_key[512];
char previous_id[64];
int64_t previous_sequence;
char host[256];
int port;
char pub[k_id_base64_len];
} invite_use_t;
static void _tf_ssb_rpc_get_previous_work(tf_ssb_connection_t* connection, void* user_data)
{
invite_use_t* work = user_data;
tf_ssb_t* ssb = work->ssb;
tf_ssb_db_get_latest_message_by_author(ssb, work->author, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
static void _tf_ssb_invite_use_pub_message_store_callback(const char* id, bool verified, bool is_new, void* user_data)
{
invite_use_t* work = user_data;
tf_free(work);
}
static void _tf_ssb_invite_use_contact_message_store_callback(const char* id, bool verified, bool is_new, void* user_data)
{
/*
** 2. Post a pub message:
** { "type": "pub", "address": { "host": "one.butt.nz", "port": 8008, "key": "@VJM7w1W19ZsKmG2KnfaoKIM66BRoreEkzaVm/J//wl8=.ed25519" } }
*/
invite_use_t* work = user_data;
tf_ssb_t* ssb = work->ssb;
JSContext* context = tf_ssb_get_context(ssb);
JSValue content = JS_NewObject(context);
JS_SetPropertyStr(context, content, "type", JS_NewString(context, "pub"));
JSValue address = JS_NewObject(context);
JS_SetPropertyStr(context, address, "host", JS_NewString(context, work->host));
JS_SetPropertyStr(context, address, "port", JS_NewInt32(context, work->port));
JS_SetPropertyStr(context, address, "key", JS_NewString(context, work->pub));
JS_SetPropertyStr(context, content, "address", address);
JSValue message = tf_ssb_sign_message(ssb, work->author, work->private_key, content, id, work->previous_sequence + 1);
JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
JS_FreeValue(context, json);
tf_ssb_verify_strip_and_store_message(ssb, message, _tf_ssb_invite_use_pub_message_store_callback, work);
JS_FreeValue(context, message);
JS_FreeValue(context, content);
}
static void _tf_ssb_rpc_get_previous_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
{
invite_use_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue content = JS_NewObject(context);
JS_SetPropertyStr(context, content, "type", JS_NewString(context, "contact"));
JS_SetPropertyStr(context, content, "contact", JS_NewString(context, work->pub));
JS_SetPropertyStr(context, content, "following", JS_TRUE);
JSValue message = tf_ssb_sign_message(ssb, work->author, work->private_key, content, work->previous_id, work->previous_sequence);
JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
JS_FreeValue(context, json);
tf_ssb_verify_strip_and_store_message(ssb, message, _tf_ssb_invite_use_contact_message_store_callback, work);
JS_FreeValue(context, message);
JS_FreeValue(context, content);
}
static void _tf_ssb_rpc_invite_use_callback(
tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{
/*
** 1. Follow the pub back:
** { "type": "contact", "contact": "@VJM7w1W19ZsKmG2KnfaoKIM66BRoreEkzaVm/J//wl8=.ed25519", "following": true }
*/
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
invite_use_t* work = tf_malloc(sizeof(invite_use_t));
*work = (invite_use_t) {
.ssb = ssb,
.port = tf_ssb_connection_get_port(connection),
};
snprintf(work->host, sizeof(work->host), "%s", tf_ssb_connection_get_host(connection));
tf_ssb_whoami(ssb, work->author, sizeof(work->author));
tf_ssb_get_private_key(ssb, work->private_key, sizeof(work->private_key));
tf_ssb_connection_get_id(connection, work->pub, sizeof(work->pub));
tf_ssb_connection_run_work(connection, _tf_ssb_rpc_get_previous_work, _tf_ssb_rpc_get_previous_after_work, work);
}
static void _tf_ssb_rpc_send_invite_use(tf_ssb_connection_t* connection)
{
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JSValue name = JS_NewArray(context);
JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "invite"));
JS_SetPropertyUint32(context, name, 1, JS_NewString(context, "use"));
JS_SetPropertyStr(context, message, "name", name);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "async"));
JSValue args = JS_NewArray(context);
JSValue object = JS_NewObject(context);
char id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, id, sizeof(id));
JS_SetPropertyStr(context, object, "feed", JS_NewString(context, id));
JS_SetPropertyUint32(context, args, 0, object);
JS_SetPropertyStr(context, message, "args", args);
tf_ssb_connection_rpc_send_json(
connection, k_ssb_rpc_flag_new_request, tf_ssb_connection_next_request_number(connection), "invite.use", message, _tf_ssb_rpc_invite_use_callback, NULL, NULL);
JS_FreeValue(context, message);
}
static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data) static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data)
{ {
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
@ -1305,6 +1340,11 @@ static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_chang
if (tf_ssb_connection_is_client(connection)) if (tf_ssb_connection_is_client(connection))
{ {
if (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_use_invite)
{
_tf_ssb_rpc_send_invite_use(connection);
}
JSValue message = JS_NewObject(context); JSValue message = JS_NewObject(context);
JSValue name = JS_NewArray(context); JSValue name = JS_NewArray(context);
JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel")); JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel"));
@ -1347,6 +1387,11 @@ static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_chang
} }
} }
JS_FreeValue(context, left); JS_FreeValue(context, left);
if (tf_ssb_connection_is_endpoint(connection) || tf_ssb_connection_is_attendant(connection))
{
_tf_ssb_rpc_send_endpoints(ssb);
}
} }
} }
} }
@ -1469,13 +1514,16 @@ static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms)
static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data)
{ {
delete_t* delete = user_data; delete_t* delete = user_data;
if (!_get_global_setting_bool(ssb, "delete_stale_feeds", false)) sqlite3* db = tf_ssb_acquire_db_reader(ssb);
bool delete_stale_feeds = false;
tf_ssb_db_get_global_setting_bool(db, "delete_stale_feeds", &delete_stale_feeds);
if (!delete_stale_feeds)
{ {
tf_ssb_release_db_reader(ssb, db);
return; return;
} }
int64_t start_ns = uv_hrtime(); int64_t start_ns = uv_hrtime();
int64_t replication_hops = 2; int64_t replication_hops = 2;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "replication_hops", &replication_hops); tf_ssb_db_get_global_setting_int64(db, "replication_hops", &replication_hops);
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops); const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops);
@ -1682,11 +1730,6 @@ static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection)
static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{ {
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
if (!tf_ssb_is_peer_exchange(ssb))
{
tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, "peers.exchange");
return;
}
_tf_ssb_rpc_peers_exchange_internal(connection, flags, request_number, args, message, size, user_data); _tf_ssb_rpc_peers_exchange_internal(connection, flags, request_number, args, message, size, user_data);
@ -1698,6 +1741,110 @@ static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t
JS_FreeValue(context, out_message); JS_FreeValue(context, out_message);
} }
typedef struct _invite_t
{
tf_ssb_connection_t* connection;
char pub[k_id_base64_len];
char invite_public_key[k_id_base64_len];
char id[k_id_base64_len];
int32_t request_number;
bool accepted;
char previous_id[256];
int64_t previous_sequence;
char* message;
} invite_t;
static void _tf_ssb_rpc_invite_use_work(tf_ssb_connection_t* connection, void* user_data)
{
invite_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
work->accepted = tf_ssb_db_use_invite(db, work->invite_public_key);
tf_ssb_release_db_writer(ssb, db);
if (work->accepted)
{
tf_ssb_db_get_latest_message_by_author(ssb, work->pub, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
}
static void _tf_ssb_invite_use_message_store_callback(const char* id, bool verified, bool is_new, void* user_data)
{
invite_t* work = user_data;
tf_ssb_connection_t* connection = work->connection;
if (verified && is_new)
{
tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, (const uint8_t*)work->message, strlen(work->message), NULL, NULL, NULL);
}
else
{
tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
}
if (work->message)
{
tf_free(work->message);
}
tf_free(work);
}
static void _tf_ssb_rpc_invite_use_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
{
invite_t* work = user_data;
if (work->accepted)
{
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue content = JS_NewObject(context);
JS_SetPropertyStr(context, content, "type", JS_NewString(context, "contact"));
JS_SetPropertyStr(context, content, "contact", JS_NewString(context, work->id));
JS_SetPropertyStr(context, content, "following", JS_TRUE);
JS_SetPropertyStr(context, content, "pub", JS_TRUE);
uint8_t private_key[512] = { 0 };
tf_ssb_get_private_key(ssb, private_key, sizeof(private_key));
JSValue message = tf_ssb_sign_message(ssb, work->pub, private_key, content, work->previous_id, work->previous_sequence);
JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
const char* string = JS_ToCString(context, json);
work->message = tf_strdup(string);
JS_FreeCString(context, string);
JS_FreeValue(context, json);
tf_ssb_verify_strip_and_store_message(ssb, message, _tf_ssb_invite_use_message_store_callback, work);
JS_FreeValue(context, message);
JS_FreeValue(context, content);
}
else
{
tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_json | k_ssb_rpc_flag_end_error, -work->request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
}
}
static void _tf_ssb_rpc_invite_use(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
{
invite_t* work = tf_malloc(sizeof(invite_t));
*work = (invite_t) {
.connection = connection,
.request_number = request_number,
};
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
tf_ssb_whoami(ssb, work->pub, sizeof(work->pub));
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue array = JS_GetPropertyStr(context, args, "args");
JSValue object = JS_GetPropertyUint32(context, array, 0);
JSValue feed = JS_GetPropertyStr(context, object, "feed");
tf_ssb_connection_get_id(connection, work->invite_public_key, sizeof(work->invite_public_key));
const char* id = JS_ToCString(context, feed);
snprintf(work->id, sizeof(work->id), "%s", id);
JS_FreeCString(context, id);
JS_FreeValue(context, feed);
JS_FreeValue(context, object);
JS_FreeValue(context, array);
tf_ssb_connection_run_work(connection, _tf_ssb_rpc_invite_use_work, _tf_ssb_rpc_invite_use_after_work, work);
}
void tf_ssb_rpc_register(tf_ssb_t* ssb) void tf_ssb_rpc_register(tf_ssb_t* ssb)
{ {
tf_ssb_add_connections_changed_callback(ssb, _tf_ssb_rpc_connections_changed_callback, NULL, NULL); tf_ssb_add_connections_changed_callback(ssb, _tf_ssb_rpc_connections_changed_callback, NULL, NULL);
@ -1708,9 +1855,11 @@ void tf_ssb_rpc_register(tf_ssb_t* ssb)
tf_ssb_add_rpc_callback(ssb, "blobs.createWants", _tf_ssb_rpc_blobs_createWants, NULL, NULL); /* SOURCE */ tf_ssb_add_rpc_callback(ssb, "blobs.createWants", _tf_ssb_rpc_blobs_createWants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "tunnel.connect", _tf_ssb_rpc_tunnel_connect, NULL, NULL); /* DUPLEX */ tf_ssb_add_rpc_callback(ssb, "tunnel.connect", _tf_ssb_rpc_tunnel_connect, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, "tunnel.isRoom", _tf_ssb_rpc_room_meta, NULL, NULL); /* FAKE-ASYNC */ tf_ssb_add_rpc_callback(ssb, "tunnel.isRoom", _tf_ssb_rpc_room_meta, NULL, NULL); /* FAKE-ASYNC */
tf_ssb_add_rpc_callback(ssb, "tunnel.endpoints", _tf_ssb_rpc_tunnel_endpoints, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "room.metadata", _tf_ssb_rpc_room_meta, NULL, NULL); /* ASYNC */ tf_ssb_add_rpc_callback(ssb, "room.metadata", _tf_ssb_rpc_room_meta, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, "room.attendants", _tf_ssb_rpc_room_attendants, NULL, NULL); /* SOURCE */ tf_ssb_add_rpc_callback(ssb, "room.attendants", _tf_ssb_rpc_room_attendants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "createHistoryStream", _tf_ssb_rpc_createHistoryStream, NULL, NULL); /* SOURCE */ tf_ssb_add_rpc_callback(ssb, "createHistoryStream", _tf_ssb_rpc_createHistoryStream, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "ebt.replicate", _tf_ssb_rpc_ebt_replicate_server, NULL, NULL); /* DUPLEX */ tf_ssb_add_rpc_callback(ssb, "ebt.replicate", _tf_ssb_rpc_ebt_replicate_server, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, "peers.exchange", _tf_ssb_rpc_peers_exchange, NULL, NULL); /* ASYNC */ tf_ssb_add_rpc_callback(ssb, "peers.exchange", _tf_ssb_rpc_peers_exchange, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, "invite.use", _tf_ssb_rpc_invite_use, NULL, NULL); /* ASYNC */
} }

View File

@ -767,7 +767,7 @@ void tf_ssb_test_bench(const tf_test_options_t* options)
static void _ssb_test_room_connections_changed(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data) static void _ssb_test_room_connections_changed(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data)
{ {
const char* changes[] = { "create", "connect", "remove" }; const char* changes[] = { "create", "connect", "remove", "update" };
tf_printf("change=%s %p connection=%s:%d\n", changes[change], connection, tf_ssb_connection_get_host(connection), tf_ssb_connection_get_port(connection)); tf_printf("change=%s %p connection=%s:%d\n", changes[change], connection, tf_ssb_connection_get_host(connection), tf_ssb_connection_get_port(connection));
} }
@ -1221,4 +1221,175 @@ void tf_ssb_test_replicate(const tf_test_options_t* options)
uv_loop_close(&loop); uv_loop_close(&loop);
} }
void tf_ssb_test_connect_str(const tf_test_options_t* options)
{
tf_printf("Testing connect string.\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);
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);
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);
tf_ssb_server_open(ssb0, 12347);
char connect[1024] = { 0 };
snprintf(connect, sizeof(connect), "net:127.0.0.1:12347~shs:%.44s", id0 + 1);
tf_printf("connect string: %s\n", connect);
tf_ssb_connect_str(ssb1, connect, 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);
tf_ssb_send_close(ssb1);
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);
}
void tf_ssb_test_invite(const tf_test_options_t* options)
{
tf_printf("Testing invites.\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);
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);
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_server_open(ssb0, 12347);
sqlite3* writer = tf_ssb_acquire_db_writer(ssb0);
char invite[1024];
tf_ssb_db_generate_invite(writer, id0, "127.0.0.1", 12347, 1, 60 * 60, invite, sizeof(invite));
tf_ssb_release_db_writer(ssb0, writer);
tf_printf("invite: %s\n", invite);
int count0 = 0;
int count1 = 0;
tf_ssb_add_message_added_callback(ssb0, _message_added, NULL, &count0);
tf_ssb_add_message_added_callback(ssb1, _message_added, NULL, &count1);
tf_ssb_connect_str(ssb1, invite, 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_printf("waiting for messages\n");
tf_ssb_set_main_thread(ssb0, true);
tf_ssb_set_main_thread(ssb1, true);
while (count0 != 3 || count1 != 3)
{
uv_run(&loop, UV_RUN_ONCE);
}
tf_ssb_set_main_thread(ssb0, false);
tf_ssb_set_main_thread(ssb1, false);
tf_printf("done\n");
tf_ssb_server_close(ssb0);
tf_ssb_send_close(ssb1);
tf_ssb_destroy(ssb0);
tf_ssb_destroy(ssb1);
tf_printf("final run\n");
uv_run(&loop, UV_RUN_DEFAULT);
tf_printf("close\n");
uv_loop_close(&loop);
}
#endif #endif

View File

@ -66,9 +66,21 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options);
void tf_ssb_test_publish(const tf_test_options_t* options); void tf_ssb_test_publish(const tf_test_options_t* options);
/** /**
** Test replication. ** Test connecting by string.
** @param options The test options. ** @param options The test options.
*/ */
void tf_ssb_test_replicate(const tf_test_options_t* options); void tf_ssb_test_replicate(const tf_test_options_t* options);
/**
** Test invites.
** @param options The test options.
*/
void tf_ssb_test_connect_str(const tf_test_options_t* options);
/**
** Test invites.
** @param options The test options.
*/
void tf_ssb_test_invite(const tf_test_options_t* options);
/** @} */ /** @} */

View File

@ -1819,6 +1819,12 @@ void tf_task_destroy(tf_task_t* task)
{ {
JS_FreeValue(task->_context, task->_exports[i]->_function); JS_FreeValue(task->_context, task->_exports[i]->_function);
} }
while (task->_import_count)
{
while (!_import_record_release(&task->_imports[task->_import_count - 1]))
{
}
}
tf_free(task->_imports); tf_free(task->_imports);
tf_free(task->_exports); tf_free(task->_exports);
task->_imports = NULL; task->_imports = NULL;
@ -1831,10 +1837,7 @@ void tf_task_destroy(tf_task_t* task)
while (task->timeouts) while (task->timeouts)
{ {
timeout_t* timeout = task->timeouts; timeout_t* timeout = task->timeouts;
if (task->_context) JS_FreeValue(task->_context, timeout->_callback);
{
JS_FreeValue(task->_context, timeout->_callback);
}
timeout->_callback = JS_UNDEFINED; timeout->_callback = JS_UNDEFINED;
_timeout_unlink(task, timeout); _timeout_unlink(task, timeout);
uv_close((uv_handle_t*)&timeout->_timer, _timeout_closed); uv_close((uv_handle_t*)&timeout->_timer, _timeout_closed);
@ -1859,14 +1862,6 @@ void tf_task_destroy(tf_task_t* task)
tf_free(task->_promises); tf_free(task->_promises);
task->_promises = NULL; task->_promises = NULL;
tf_free(task->_imports);
task->_imports = NULL;
task->_import_count = 0;
tf_free(task->_exports);
task->_exports = NULL;
task->_export_count = 0;
if (task->_db) if (task->_db)
{ {
sqlite3_close(task->_db); sqlite3_close(task->_db);

View File

@ -1079,6 +1079,8 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false); _tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
_tf_test_run(options, "publish", tf_ssb_test_publish, false); _tf_test_run(options, "publish", tf_ssb_test_publish, false);
_tf_test_run(options, "replicate", tf_ssb_test_replicate, false); _tf_test_run(options, "replicate", tf_ssb_test_replicate, false);
_tf_test_run(options, "connect_str", tf_ssb_test_connect_str, false);
_tf_test_run(options, "invite", tf_ssb_test_invite, false);
tf_printf("Tests completed.\n"); tf_printf("Tests completed.\n");
#endif #endif
} }

View File

@ -34,6 +34,7 @@ typedef struct _tf_trace_stack_t
typedef struct _tf_trace_thread_t typedef struct _tf_trace_thread_t
{ {
pthread_t id; pthread_t id;
int index;
tf_trace_stack_t* stack; tf_trace_stack_t* stack;
char name[64]; char name[64];
} tf_trace_thread_t; } tf_trace_thread_t;
@ -208,6 +209,7 @@ static tf_trace_thread_t* _tf_trace_get_thread(tf_trace_t* trace, pthread_t self
found = tf_malloc(sizeof(tf_trace_thread_t)); found = tf_malloc(sizeof(tf_trace_thread_t));
*found = (tf_trace_thread_t) { *found = (tf_trace_thread_t) {
.id = self, .id = self,
.index = trace->threads_count,
}; };
#if defined(__linux__) && !defined(__ANDROID__) #if defined(__linux__) && !defined(__ANDROID__)
pthread_getname_np(self, found->name, sizeof(found->name)); pthread_getname_np(self, found->name, sizeof(found->name));
@ -221,7 +223,7 @@ static tf_trace_thread_t* _tf_trace_get_thread(tf_trace_t* trace, pthread_t self
return found; return found;
} }
static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name, void* tag) static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name, void* tag, tf_trace_thread_t** out_thread)
{ {
tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self); tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self);
if (!thread->stack || thread->stack->count + 1 > tf_countof(thread->stack->names)) if (!thread->stack || thread->stack->count + 1 > tf_countof(thread->stack->names))
@ -239,9 +241,10 @@ static void _tf_push_stack(tf_trace_t* trace, pthread_t self, const char* name,
stack->names[stack->count] = name; stack->names[stack->count] = name;
stack->tags[stack->count] = tag; stack->tags[stack->count] = tag;
stack->count++; stack->count++;
*out_thread = thread;
} }
static const char* _tf_pop_stack(tf_trace_t* trace, pthread_t self, void* tag) static const char* _tf_pop_stack(tf_trace_t* trace, pthread_t self, void* tag, tf_trace_thread_t** out_thread)
{ {
tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self); tf_trace_thread_t* thread = _tf_trace_get_thread(trace, self);
tf_trace_stack_t* stack = thread->stack; tf_trace_stack_t* stack = thread->stack;
@ -255,6 +258,7 @@ static const char* _tf_pop_stack(tf_trace_t* trace, pthread_t self, void* tag)
name = stack->names[stack->count - 1]; name = stack->names[stack->count - 1];
stack->count--; stack->count--;
} }
*out_thread = thread;
return name; return name;
} }
@ -265,11 +269,11 @@ static void _tf_trace_begin_tagged(tf_trace_t* trace, const char* name, void* ta
return; return;
} }
pthread_t self = pthread_self(); tf_trace_thread_t* thread = NULL;
_tf_push_stack(trace, self, name, tag); _tf_push_stack(trace, pthread_self(), name, tag, &thread);
char line[1024]; char line[1024];
int p = snprintf(line, sizeof(line), "{\"ph\": \"B\", \"pid\": %d, \"tid\": %" PRId64 ", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)self, _trace_ts()); int p = snprintf(line, sizeof(line), "{\"ph\": \"B\", \"pid\": %d, \"tid\": %d, \"ts\": %" PRId64 ", \"name\": \"", getpid(), thread->index, _trace_ts());
p += _tf_trace_escape_name(line + p, sizeof(line) - p - 4, name); p += _tf_trace_escape_name(line + p, sizeof(line) - p - 4, name);
p += snprintf(line + p, sizeof(line) - p, "\"},"); p += snprintf(line + p, sizeof(line) - p, "\"},");
p = tf_min(p, tf_countof(line)); p = tf_min(p, tf_countof(line));
@ -288,15 +292,15 @@ static void _tf_trace_end_tagged(tf_trace_t* trace, void* tag)
return; return;
} }
pthread_t self = pthread_self(); tf_trace_thread_t* thread = NULL;
const char* name = _tf_pop_stack(trace, self, tag); const char* name = _tf_pop_stack(trace, pthread_self(), tag, &thread);
if (!name) if (!name)
{ {
return; return;
} }
char line[1024]; char line[1024];
int p = snprintf(line, sizeof(line), "{\"ph\": \"E\", \"pid\": %d, \"tid\": %" PRId64 ", \"ts\": %" PRId64 ", \"name\": \"", getpid(), (int64_t)pthread_self(), _trace_ts()); int p = snprintf(line, sizeof(line), "{\"ph\": \"E\", \"pid\": %d, \"tid\": %d, \"ts\": %" PRId64 ", \"name\": \"", getpid(), thread->index, _trace_ts());
p += _tf_trace_escape_name(line + p, sizeof(line) - p - 4, name); p += _tf_trace_escape_name(line + p, sizeof(line) - p - 4, name);
p += snprintf(line + p, sizeof(line) - p, "\"},"); p += snprintf(line + p, sizeof(line) - p, "\"},");
p = tf_min(p, tf_countof(line)); p = tf_min(p, tf_countof(line));
@ -330,8 +334,8 @@ char* tf_trace_export(tf_trace_t* trace)
for (int i = 0; i < trace->threads_count; i++) for (int i = 0; i < trace->threads_count; i++)
{ {
tf_trace_thread_t* thread = trace->threads[i]; tf_trace_thread_t* thread = trace->threads[i];
size += snprintf(buffer + size, k_buffer_size - size, "{\"ph\":\"M\",\"pid\":%d,\"tid\":%" PRId64 ",\"name\":\"thread_name\",\"args\":{\"name\":\"%s\"}},\n", getpid(), size += snprintf(buffer + size, k_buffer_size - size, "{\"ph\":\"M\",\"pid\":%d,\"tid\":%d,\"name\":\"thread_name\",\"args\":{\"name\":\"%s\"}},\n", getpid(),
(uint64_t)thread->id, thread->name); thread->index, thread->name);
} }
uv_rwlock_rdunlock(&trace->threads_lock); uv_rwlock_rdunlock(&trace->threads_lock);
if (begin) if (begin)

View File

@ -20,6 +20,16 @@
#include <execinfo.h> #include <execinfo.h>
#endif #endif
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
#define TF_IS_MOBILE 1
#else
#define TF_IS_MOBILE 0
#endif
static JSValue _util_utf8_encode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _util_utf8_encode(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
size_t length = 0; size_t length = 0;
@ -304,69 +314,139 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
return result; return result;
} }
#if defined(__APPLE__) typedef enum _value_kind_t
#include <TargetConditionals.h>
#endif
static bool _is_mobile()
{ {
#if defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IPHONE) k_kind_bool,
return true; k_kind_int,
#else k_kind_string,
return false; } value_kind_t;
#endif
static const char* k_kind_name[] = {
[k_kind_bool] = "bool",
[k_kind_int] = "int",
[k_kind_string] = "string",
};
typedef struct _setting_value_t
{
value_kind_t kind;
union
{
bool bool_value;
int int_value;
const char* string_value;
};
} setting_value_t;
typedef struct _setting_t
{
const char* name;
const char* type;
const char* description;
setting_value_t default_value;
} setting_t;
static const setting_t k_settings[] = {
{ .name = "code_of_conduct", .type = "textarea", .description = "Code of conduct presented at sign-in.", .default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "blob_fetch_age_seconds",
.type = "integer",
.description = "Only blobs mentioned more recently than this age will be automatically fetched.",
.default_value = { .kind = k_kind_int, .int_value = TF_IS_MOBILE ? (int)(0.5f * 365 * 24 * 60 * 60) : -1 } },
{ .name = "blob_expire_age_seconds",
.type = "integer",
.description = "Blobs older than this will be automatically deleted.",
.default_value = { .kind = k_kind_int, .int_value = TF_IS_MOBILE ? (int)(1.0f * 365 * 24 * 60 * 60) : -1 } },
{ .name = "fetch_hosts",
.type = "string",
.description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.",
.default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "http_redirect",
.type = "string",
.description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")",
.default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "index", .type = "string", .description = "Default path.", .default_value = { .kind = k_kind_string, .string_value = "/~core/ssb/" } },
{ .name = "index_map",
.type = "textarea",
.description = "Mappings from hostname to redirect path, one per line, as in: \"www.tildefriends.net=/~core/index/\"",
.default_value = { .kind = k_kind_string, .string_value = NULL } },
{ .name = "peer_exchange",
.type = "boolean",
.description = "Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.",
.default_value = { .kind = k_kind_bool, .bool_value = false } },
{ .name = "replicator", .type = "boolean", .description = "Enable message and blob replication.", .default_value = { .kind = k_kind_bool, .bool_value = true } },
{ .name = "room", .type = "boolean", .description = "Enable peers to tunnel through this instance as a room.", .default_value = { .kind = k_kind_bool, .bool_value = true } },
{ .name = "room_name", .type = "string", .description = "Name of the room.", .default_value = { .kind = k_kind_string, .string_value = "tilde friends tunnel" } },
{ .name = "seeds_host",
.type = "string",
.description = "Hostname for seed connections.",
.default_value = { .kind = k_kind_string, .string_value = "seeds.tildefriends.net" } },
{ .name = "account_registration", .type = "boolean", .description = "Allow registration of new accounts.", .default_value = { .kind = k_kind_bool, .bool_value = true } },
{ .name = "replication_hops",
.type = "integer",
.description = "Number of hops to replicate (1 = direct follows, 2 = follows of follows, etc.).",
.default_value = { .kind = k_kind_int, .int_value = 2 } },
{ .name = "delete_stale_feeds",
.type = "boolean",
.description = "Periodically delete feeds that aren't visible from local accounts or related follows.",
.default_value = { .kind = k_kind_bool, .bool_value = false } },
{ .name = "talk_to_strangers",
.type = "boolean",
.description = "Whether connections are accepted from accounts that aren't in the replication range or otherwise already known.",
.default_value = { .kind = k_kind_bool, .bool_value = true } },
};
static const setting_t* _util_get_setting(const char* name, value_kind_t kind)
{
for (int i = 0; i < tf_countof(k_settings); i++)
{
if (strcmp(k_settings[i].name, name) == 0 && k_settings[i].default_value.kind == kind)
{
return &k_settings[i];
}
}
tf_printf("Did not find global setting of type %s: %s.\n", k_kind_name[kind], name);
return NULL;
}
bool tf_util_get_default_global_setting_bool(const char* name)
{
const setting_t* setting = _util_get_setting(name, k_kind_bool);
return setting ? setting->default_value.bool_value : false;
}
int tf_util_get_default_global_setting_int(const char* name)
{
const setting_t* setting = _util_get_setting(name, k_kind_int);
return setting ? setting->default_value.int_value : 0;
}
const char* tf_util_get_default_global_setting_string(const char* name)
{
const setting_t* setting = _util_get_setting(name, k_kind_string);
return setting && setting->default_value.string_value ? setting->default_value.string_value : "";
} }
static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
typedef struct _setting_t
{
const char* name;
const char* type;
const char* description;
JSValue default_value;
} setting_t;
const setting_t k_settings[] = {
{ .name = "code_of_conduct", .type = "textarea", .description = "Code of conduct presented at sign-in." },
{ .name = "blob_fetch_age_seconds",
.type = "integer",
.description = "Only blobs mentioned more recently than this age will be automatically fetched.",
.default_value = _is_mobile() ? JS_NewInt32(context, (int)(0.5f * 365 * 24 * 60 * 60)) : JS_UNDEFINED },
{ .name = "blob_expire_age_seconds",
.type = "integer",
.description = "Blobs older than this will be automatically deleted.",
.default_value = _is_mobile() ? JS_NewInt32(context, (int)(1.0f * 365 * 24 * 60 * 60)) : JS_UNDEFINED },
{ .name = "fetch_hosts", .type = "string", .description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty." },
{ .name = "http_redirect", .type = "string", .description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")" },
{ .name = "index", .type = "string", .description = "Default path.", .default_value = JS_NewString(context, "/~core/apps") },
{ .name = "index_map", .type = "textarea", .description = "Mappings from hostname to redirect path, one per line, as in: \"www.tildefriends.net=/~core/index/\"" },
{ .name = "peer_exchange",
.type = "boolean",
.description = "Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.",
.default_value = JS_FALSE },
{ .name = "replicator", .type = "boolean", .description = "Enable message and blob replication.", .default_value = JS_TRUE },
{ .name = "room", .type = "boolean", .description = "Enable peers to tunnel through this instance as a room.", .default_value = JS_TRUE },
{ .name = "room_name", .type = "string", .description = "Name of the room.", .default_value = JS_NewString(context, "tilde friends tunnel") },
{ .name = "seeds_host", .type = "string", .description = "Hostname for seed connections.", .default_value = JS_NewString(context, "seeds.tildefriends.net") },
{ .name = "account_registration", .type = "boolean", .description = "Allow registration of new accounts.", .default_value = JS_TRUE },
{ .name = "replication_hops",
.type = "integer",
.description = "Number of hops to replicate (1 = direct follows, 2 = follows of follows, etc.).",
.default_value = JS_NewInt32(context, 2) },
{ .name = "delete_stale_feeds",
.type = "boolean",
.description = "Periodically delete feeds that visible from local accounts and related follows.",
.default_value = JS_FALSE },
};
JSValue settings = JS_NewObject(context); JSValue settings = JS_NewObject(context);
for (int i = 0; i < tf_countof(k_settings); i++) for (int i = 0; i < tf_countof(k_settings); i++)
{ {
JSValue entry = JS_NewObject(context); JSValue entry = JS_NewObject(context);
JS_SetPropertyStr(context, entry, "type", JS_NewString(context, k_settings[i].type)); JS_SetPropertyStr(context, entry, "type", JS_NewString(context, k_settings[i].type));
JS_SetPropertyStr(context, entry, "description", JS_NewString(context, k_settings[i].description)); JS_SetPropertyStr(context, entry, "description", JS_NewString(context, k_settings[i].description));
JS_SetPropertyStr(context, entry, "default_value", k_settings[i].default_value); switch (k_settings[i].default_value.kind)
{
case k_kind_bool:
JS_SetPropertyStr(context, entry, "default_value", JS_NewBool(context, k_settings[i].default_value.bool_value));
break;
case k_kind_int:
JS_SetPropertyStr(context, entry, "default_value", JS_NewInt32(context, k_settings[i].default_value.int_value));
break;
case k_kind_string:
JS_SetPropertyStr(
context, entry, "default_value", k_settings[i].default_value.string_value ? JS_NewString(context, k_settings[i].default_value.string_value) : JS_UNDEFINED);
break;
}
JS_SetPropertyStr(context, settings, k_settings[i].name, entry); JS_SetPropertyStr(context, settings, k_settings[i].name, entry);
} }
return settings; return settings;

View File

@ -10,6 +10,9 @@
#include <stdbool.h> #include <stdbool.h>
/** An event loop. */
typedef struct uv_loop_s uv_loop_t;
/** /**
** Register utility script functions. ** Register utility script functions.
** @param context The JS context. ** @param context The JS context.
@ -150,6 +153,19 @@ const char* tf_util_function_to_string(void* function);
_a > _b ? _b : _a; \ _a > _b ? _b : _a; \
}) })
/**
** Get the maximum of two values.
** @param a The first value.
** @param b The second value.
** @return The maximum of a and b.
*/
#define tf_max(a, b) \
({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
/** /**
** Get the number of elements in an array. ** Get the number of elements in an array.
** @param a The array. ** @param a The array.
@ -157,4 +173,25 @@ const char* tf_util_function_to_string(void* function);
*/ */
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a)))) #define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
/**
** Get the default value of a global setting as a boolean.
** @param name The setting name.
** @return The default value.
*/
bool tf_util_get_default_global_setting_bool(const char* name);
/**
** Get the default value of a global setting as an integer.
** @param name The setting name.
** @return The default value.
*/
int tf_util_get_default_global_setting_int(const char* name);
/**
** Get teh default value of a global setting as a string.
** @param name The setting name.
** @return The default value.
*/
const char* tf_util_get_default_global_setting_string(const char* name);
/** @} */ /** @} */

3
test.c
View File

@ -1,3 +0,0 @@
int main() {
return 0;
}

View File

@ -71,7 +71,7 @@ try:
driver = webdriver.Firefox(options = options, service = service) driver = webdriver.Firefox(options = options, service = service)
wait = WebDriverWait(driver, 10) wait = WebDriverWait(driver, 10)
driver.get('http://localhost:8888') driver.get('http://localhost:8888/~core/apps/')
select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('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', '#name'], ('send_keys', 'adminuser'))
@ -89,7 +89,7 @@ try:
select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
driver.get('http://localhost:8888') driver.get('http://localhost:8888/~core/apps/')
select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('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', '#name'], ('send_keys', 'testuser'))
@ -140,7 +140,7 @@ try:
select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',))
driver.get('http://localhost:8888') driver.get('http://localhost:8888/~core/apps/')
select(driver, ['#document', 'frame', '=identity']) select(driver, ['#document', 'frame', '=identity'])
@ -170,7 +170,7 @@ try:
id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1] id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
assert id0 == id1 assert id0 == id1
driver.get('http://localhost:8888') driver.get('http://localhost:8888/~core/apps/')
select(driver, ['#document', 'frame', '=ssb'], ('click',)) 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', '#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, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))

View File

@ -12,6 +12,7 @@ else
fi fi
rm -rf $WORK_DIR rm -rf $WORK_DIR
mkdir -p out/
cp -aRf deps/openssl_src/ $WORK_DIR cp -aRf deps/openssl_src/ $WORK_DIR
echo "Building" echo "Building"
@ -73,6 +74,7 @@ no-weak-ssl-ciphers
no-zlib no-zlib
-Os -Os
-DOPENSSL_SMALL_FOOTPRINT -DOPENSSL_SMALL_FOOTPRINT
-Wno-error
-ffunction-sections -ffunction-sections
-fdata-sections" -fdata-sections"
pwd pwd