Compare commits

...

204 Commits
main ... main

Author SHA1 Message Date
20e60262fd build: Some clangs are more strict than quickjs. 2025-02-09 21:46:17 -05:00
9e3928c976 build: Some clangs are more strict about this. quickjs is not. 2025-02-09 21:45:05 -05:00
95d8768545 build: Try to get CI building for macos. 2025-02-09 20:47:20 -05:00
8679d09040 build: Build a fat macos binary. 2025-02-09 08:13:13 -05:00
4cc5c6acb3 build: On second thought, split the cross-build of macos into x86_64 and arm. 2025-02-06 12:48:17 -05:00
6f9b548b1a build: There seems to be a way to build for macos from Linux. Neat. 2025-02-05 20:50:41 -05:00
fbff3386a9 update: CoreMirror. 2025-02-05 18:58:03 -05:00
e5899fca58 ssb: Use the cache of private messages we built for the unread notification to actually show private messages. Still needs some work, but it's something. 2025-02-05 18:41:37 -05:00
dddec489b9 ssb: Results of staring at unexpected shs disconnections: wire up tunnel.endpoints and blobs.createWants more correctly/thoroughly. And add slightly more context when deliberately disconnected from the remote side. 2025-02-04 21:32:22 -05:00
b4049eaeaa welcome: Feeling fancy. Might revert later. 2025-02-04 12:53:04 -05:00
b3fd724b5a docs: Some updates to the README. 2025-02-04 12:39:59 -05:00
aca25be86a docs: Minor cleanup/normalization. 2025-02-03 12:39:23 -05:00
0f8cbdac57 docs: Bring back docs as a directory. Long live https://docs.tildefriends.net/ 2025-02-02 22:39:55 -05:00
630219d667 docs: Stop using docs as a submodule. It discouraged pull requests. 2025-02-02 22:39:11 -05:00
fef268e434 build: Oops. 2025-02-02 22:21:16 -05:00
e18dcf3a48 build: Maybe ssh? 2025-02-02 22:18:05 -05:00
e019320146 build: Forgot the actual rsync options I wanted. 2025-02-02 21:55:57 -05:00
65b31e14f9 build: +rsync. Formatting yaml. 2025-02-02 21:51:18 -05:00
9cb5cbcde9 build: OK, that fail's on me. 2025-02-02 21:47:05 -05:00
25914ff5a7 build: c'mon 2025-02-02 21:44:05 -05:00
c4af799279 build: Let's try to put docs somewhere. 2025-02-02 21:40:29 -05:00
3f8f0e14f4 build: Make a directory for a thing that fails intermittently?? 2025-02-02 17:12:03 -05:00
5414b30e7f build: Resolve some zsign/cmake warnings. 2025-02-02 17:07:59 -05:00
7aee897c1b cleanup: No longer need form parsing in JS. 2025-02-02 16:52:44 -05:00
4060f9cc11 build: jarsigner -> apksigner to suppress a warning. 2025-02-02 16:38:55 -05:00
5b526cbf5b build: Clean up / consolidate some OpenSSL options now that there is only one place. Also: deadstrip better? 2025-02-02 16:19:46 -05:00
f5065ff42b build: Indicate that we always use system OpenSSL on Haiku and BSD. 2025-02-02 21:08:46 -05:00
86b5546f5f build: Get rid of the ios-specific OpenSSL build script. Now there is only one. 2025-02-02 15:56:06 -05:00
43ae2a293b build: Fix cross-compiling for arm. 2025-02-02 15:55:12 -05:00
b8ddbd4255 build: Remove the mingw-specific OpenSSL build script. 2025-02-02 15:21:38 -05:00
87cdba1db8 build: Replace the android-specific script to build OpenSSL with the shared one. 2025-02-02 15:16:51 -05:00
df83187e33 cleanup: Remove File.writeFile(). It is no longer used or necessary. 2025-02-02 14:52:25 -05:00
eccdbf29ab build: Move built openssl to out where it belongs. 2025-02-02 14:39:36 -05:00
28d181f8bc build: Fix. 2025-02-02 14:28:40 -05:00
d386daf2ff build: Rename the macosdebug/macosrelease targets to debug/release. #101 2025-02-02 14:08:27 -05:00
0c1e116c1e build: More concise build context. 2025-02-02 13:04:22 -05:00
bb0ed67827 httpd: Move starting the http server into C. 2025-02-02 12:27:06 -05:00
b111d06851 ssb: I think this fixes missing a range in some circumstances. 2025-02-02 10:14:14 -05:00
79388845ea build: Remove the dependency on zsign on macos. 2025-02-01 22:30:35 -05:00
99faef2e77 build: valid bash? 2025-02-01 19:59:18 -05:00
21fffbfe10 build: Valid yaml? 2025-02-01 19:58:28 -05:00
64fb9f0c2a build: +zlib. 2025-02-01 19:55:02 -05:00
a42e0bef2c build: The journey continues. 2025-02-01 19:15:50 -05:00
45a09006e1 build: Build zsign for the sake of the ios build. 2025-02-01 18:52:20 -05:00
240484be4c build: Fix docker build, and speed it up a bit? 2025-02-01 18:19:44 -05:00
22f4d115e3 build: Finally ios is happy. Now let's un-break docker. 2025-02-01 18:07:56 -05:00
32920e0e5d build: Still trying to get ios to build on this worker. 2025-02-01 17:58:55 -05:00
f03a5918d1 build: Apparently we need file. 2025-02-01 17:19:28 -05:00
dd1870b52a build: Nope, OpenSSL assumes this directory is SDKs, so it is SDKs. 2025-02-01 16:51:10 -05:00
f0c1a8f98f build: Minor cleanup and a disappointing workaround for iOS cross-compiling toolchain issues I don't understand. 2025-02-01 16:25:23 -05:00
0c181d7e7c build: Maybe maybe maybe. 2025-02-01 14:55:26 -05:00
e3dc0e833a build: Some docs say the syntax is like this. 2025-02-01 14:17:15 -05:00
6de875edea build: Print some configuration that is confusing me. 2025-02-01 13:38:52 -05:00
986a55173f build: I think CI can cross-build ios. 2025-02-01 12:50:31 -05:00
59c8cabf02 build: fix? 2025-02-01 11:39:58 -05:00
a33b9fab07 build: I misunderstood these makefile conditionals. 2025-02-01 11:32:01 -05:00
f8a725e1e7 build: ios dist is conditional on having the SDK. 2025-02-01 11:02:21 -05:00
b3ab3af01b build: dist uses curl. 2025-02-01 10:10:59 -05:00
79f9463e56 build: Can I get CI to artifact the dist files? 2025-02-01 09:49:49 -05:00
4257b2ed51 ssb: Unknown message types need some padding. 2025-02-01 09:41:22 -05:00
6488ab60ec build: Docker build no longer needs libssl installed. 2025-02-01 09:40:34 -05:00
18bd3dfcf9 build: So close. +docker? 2025-02-01 09:00:06 -05:00
ec114e160d build: on node:23-bookworm-slim 2025-02-01 08:34:42 -05:00
de033af18f build: Oh, now I can use a normal image, maybe with a more reasonable compiler? Also, update libbacktrace. 2025-02-01 08:31:33 -05:00
e971c6fcb7 build: Go home, -fanalyze, you're confused. 2025-02-01 08:14:11 -05:00
d3c465391c build: +zip 2025-01-31 21:47:24 -05:00
f1fa19593d build: Better system OpenSSL logic? 2025-01-31 21:43:18 -05:00
60b6f9c73e build: +build-essential 2025-01-31 21:32:17 -05:00
55b95ddecb build: +make 2025-01-31 21:27:14 -05:00
0800a251b2 build: +unzip 2025-01-31 21:23:22 -05:00
82cf7a80eb build: Maybe I don't need sudo here? 2025-01-31 21:19:22 -05:00
379f3d12eb build: Maybe we can just install the git we need? 2025-01-31 21:15:19 -05:00
e52972d4d4 build: Version shenanigans? 2025-01-31 21:11:35 -05:00
1a0ca4dec2 build: I guess this requires node? 2025-01-31 21:08:57 -05:00
0bac9d8d5a build: Can I specify the image we build on like this? 2025-01-31 21:07:01 -05:00
a9608363c5 core: Don't actually start the http server unless we've specified a port or are asking for a port to be determined and written to file. 2025-01-31 20:57:59 -05:00
f1a2c5ae8e ssb: Silence some persistent broadcast noise. 2025-01-31 20:45:35 -05:00
192a81ede7 core: As an experiment, handle running from a subdirectory (generally out/debug or out/release, since people tend to do that). Remove unused File.stat(). 2025-01-31 20:37:14 -05:00
916aa5abbd cli: Fix create_invite's expire option. 2025-01-30 22:16:06 -05:00
d4a5cc6eee build: Let's start work on 0.0.28. 2025-01-30 12:46:52 -05:00
d19605cc8d build: Fixes nix. Now everybody is happy. 2025-01-30 12:35:43 -05:00
8d529327a4 build: Let's go back to nix trying to use its own OpenSSL. 2025-01-30 12:31:12 -05:00
697e2f2071 build: Nope, I don't understand nix. 2025-01-29 20:53:09 -05:00
f7fb112f21 build: I don't understand this nix error. 2025-01-29 20:32:35 -05:00
b2c0211190 build: nix? 2025-01-29 20:28:26 -05:00
c59e0ea6e5 build: Let's release 0.0.27. 2025-01-29 19:58:31 -05:00
6c2fc1444d ssb: Fix some SQL syntax for private messages. They need more work. 2025-01-29 12:15:46 -05:00
94d969fe46 update: CodeMirror. 2025-01-28 22:23:52 -05:00
a7bc3d301c cleanup: format. 2025-01-28 22:00:48 -05:00
18bab849f7 ssb: prettier. 2025-01-27 21:02:15 -05:00
04878fcc30 ssb: Fix multiple bugs with follow status lying. 2025-01-27 20:28:44 -05:00
ffe1299548 ssb: More room for activities in the emoji picker dialog. 2025-01-27 12:45:54 -05:00
64bdbf5725 build: Never mind. flatpak tools don't want to work in docker. 2025-01-27 12:11:41 -05:00
b82deb557f build: https://github.com/flatpak/flatpak-builder/issues/237 ?? 2025-01-26 21:43:18 -05:00
d529a48a11 build: flatpak maybe? 2025-01-26 21:14:55 -05:00
b711e4e6bd build: Can the CI worker build the flatpak? Let's find out. 2025-01-26 20:47:44 -05:00
186eecfbff build: I don't know why this works better, but it does. 2025-01-26 20:43:02 -05:00
d766c33f59 build: Stop using lto. Not worth the hassle. 2025-01-26 09:56:53 -05:00
2c9257f1a8 build: Trying to understand a build thing. 2025-01-26 09:33:45 -05:00
71ff604f90 ssb: Stop breaking up the news queries into chunks. It sometimes produced incorrect results, and it is not necessary if we're fast enough. 2025-01-25 20:13:05 -05:00
83e00763ea ssb: Be a bit more aggressive about loading latest messages. It bugs me when I don't see my own reactions in a channel or whatnot, and I think it has to do with the queried time ranges. 2025-01-25 18:00:06 -05:00
5b647e0937 buttfeed: Use markdown that works better in Manyverse. 2025-01-25 08:08:02 -05:00
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
105 changed files with 6733 additions and 3828 deletions

View File

@ -1,4 +1,3 @@
.svn
db.sqlite
out/**/*.o
out/**/*.d
.git
db.sqlite*
out/

View File

@ -6,15 +6,49 @@ jobs:
Build-All:
runs-on: ubuntu-latest
container:
valid_volumes: ['/opt/keys']
image: node:23-bookworm-slim
valid_volumes:
- '/opt/keys'
- '/opt/deps'
volumes:
- /opt/keys:/opt/keys
- /opt/deps:/opt/deps
steps:
- name: check out code
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
clang \
cmake \
curl \
docker.io \
doxygen \
file \
gcc-aarch64-linux-gnu \
git \
graphviz \
libgpgme11 \
libssl-dev \
mingw-w64 \
rsync \
unzip \
zip \
zlib1g-dev
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- run: ln -s /opt/keys .keys
- name: Setup environment
run: |
ln -s /opt/keys .keys
ln -s /opt/deps/ios_toolchain deps/ios_toolchain
ln -s /opt/deps/macos_toolchain deps/macos_toolchain
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
- name: Setup JDK
uses: actions/setup-java@v3
with:
@ -24,14 +58,12 @@ jobs:
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11 gcc-aarch64-linux-gnu
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
- run: docker build .
- uses: actions/upload-artifact@v3
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
path: |
out/TildeFriends-release.fdroid.apk
out/winrelease/tildefriends.exe
out/tildefriends-x86_64.AppImage
out/release/tildefriends.standalone
out/armrelease/tildefriends.standalone
name: dist
path: dist/*

3
.gitignore vendored
View File

@ -1,7 +1,8 @@
build/
*.core
db.*
deps/ios_toolchain/
deps/ios_toolchain
deps/macos_toolchain
deps/openssl/
dist/
.flatpak-builder

6
.gitmodules vendored
View File

@ -26,6 +26,6 @@
[submodule "deps/c-ares"]
path = deps/c-ares
url = https://github.com/c-ares/c-ares.git
[submodule "docs"]
path = docs
url = https://dev.tildefriends.net/cory/tildefriends.wiki.git
[submodule "deps/zsign"]
path = deps/zsign
url = https://github.com/zhlynn/zsign.git

View File

@ -1,19 +1,16 @@
FROM bitnami/minideb:bullseye AS build
FROM bitnami/minideb:bookworm AS build
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
libssl-dev \
perl \
make
COPY . /app
RUN make -C /app -j $(nproc) release
FROM bitnami/minideb:bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libssl1.1
FROM bitnami/minideb:bookworm
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
COPY --from=build /app/apps /app/apps

View File

@ -943,7 +943,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
INPUT = src/
INPUT = README.md docs/ src/
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
@ -1110,7 +1110,7 @@ FILTER_SOURCE_PATTERNS =
# (index.html). This can be useful if you have a project on for instance GitHub
# and want to reuse the introduction page also for the doxygen output.
USE_MDFILE_AS_MAINPAGE =
USE_MDFILE_AS_MAINPAGE = README.md
# The Fortran standard specifies that for fixed formatted Fortran code all
# characters from position 72 are to be considered as comment. A common

View File

@ -16,11 +16,11 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 32
VERSION_NUMBER := 0.0.27-wip
VERSION_CODE := 33
VERSION_NUMBER := 0.0.28-wip
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
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
@ -33,21 +33,23 @@ UNAME_M := $(shell uname -m)
ANDROID_SDK ?= ~/Android/Sdk
BUNDLETOOL = out/bundletool.jar
HAVE_WIN := 0
HAVE_CROSS_AARCH64 := 0
HAVE_WIN :=
HAVE_CROSS_AARCH64 :=
USE_SYSTEM_SSL :=
export SOURCE_DATE_EPOCH=1
export TZ=UTC
ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
BUILD_TYPES := debug release iosdebug iosrelease iossimdebug iossimrelease
else ifeq ($(UNAME_S),Linux)
BUILD_TYPES := debug release
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1)
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1)
HAVE_LINUX_MACOS = $(if $(shell which deps/macos_toolchain/bin/oa64-clang),1)
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1)
ifneq ($(UNAME_M),aarch64)
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1,0)
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1)
endif
else ifeq ($(UNAME_S),Haiku)
BUILD_TYPES := debug release
@ -56,6 +58,7 @@ LDFLAGS += \
-lbsd \
-lnetwork \
-Wno-stringop-overflow
USE_SYSTEM_SSL := 1
else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release
CFLAGS += \
@ -63,18 +66,24 @@ CFLAGS += \
LDFLAGS += \
-lexecinfo \
-lc++abi
HAVE_ANDROID := 0
HAVE_LINUX_IOS := 0
HAVE_ANDROID :=
HAVE_LINUX_IOS :=
HAVE_LINUX_MACOS :=
USE_SYSTEM_SSL := 1
else
$(error Unexpected host platform $(UNAME_S).)
endif
# Everything is set above.
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0) system_ssl=$(if $(USE_SYSTEM_SSL),1,0))
CFLAGS += \
-std=gnu11 \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-cast-function-type-mismatch \
-Wno-unknown-warning-option \
-Wno-unused-parameter \
-MMD \
-MP \
-ffunction-sections \
@ -83,8 +92,7 @@ CFLAGS += \
-g
LDFLAGS += \
-Wno-attributes \
-Wno-aggressive-loop-optimizations \
-flto=auto
-Wno-aggressive-loop-optimizations
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
@ -127,6 +135,7 @@ WINDOWS_TARGETS := \
out/winrelease/tildefriends.exe
ifeq ($(HAVE_WIN),1)
BUILD_TYPES += windebug winrelease
all: out/windebug/tildefriends.standalone.exe out/winrelease/tildefriends.standalone.exe
endif
AARCH64_TARGETS := \
@ -136,12 +145,28 @@ ifeq ($(HAVE_CROSS_AARCH64),1)
BUILD_TYPES += armdebug armrelease
endif
LINUX_TARGETS := \
HOST_TARGETS := \
out/debug/tildefriends \
out/release/tildefriends
ifeq ($(UNAME_S),Darwin)
MACOS_TARGETS := \
out/macosdebug/tildefriends \
out/macosrelease/tildefriends
out/debug/tildefriends \
out/release/tildefriends
else ifeq ($(UNAME_S),Linux)
ifeq ($(HAVE_LINUX_MACOS),1)
MACOS_TARGETS := \
out/macosdebug-arm/tildefriends \
out/macosrelease-arm/tildefriends \
out/macosdebug-x86_64/tildefriends \
out/macosrelease-x86_64/tildefriends
all: out/macosdebug/tildefriends.standalone
all: out/macosrelease/tildefriends.standalone
else
MACOS_TARGETS :=
endif
else
MACOS_TARGETS :=
endif
IOS_TARGETS := \
out/iosdebug/tildefriends \
out/iosrelease/tildefriends
@ -155,6 +180,14 @@ ifeq ($(HAVE_LINUX_IOS),1)
BUILD_TYPES += iosdebug iosrelease
all: $(IOS_APPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
BUILD_TYPES += \
macosdebug-arm \
macosrelease-arm \
macosdebug-x86_64 \
macosrelease-x86_64
all: $(IOS_APPS)
endif
ifeq ($(UNAME_S),Darwin)
all: $(IOS_APPS) \
out/tildefriends-iossimdebug.app/tildefriends \
@ -169,29 +202,30 @@ DEBUG_TARGETS := \
out/windebug/tildefriends.exe \
out/iosdebug/tildefriends \
out/iossimdebug/tildefriends \
out/macosdebug/tildefriends \
out/androiddebug/tildefriends \
out/androiddebug-armv7a/tildefriends \
out/androiddebug-x86_64/tildefriends \
out/androiddebug-x86/tildefriends \
out/armdebug/tildefriends
out/armdebug/tildefriends \
out/macosdebug-arm/tildefriends \
out/macosdebug-x86_64/tildefriends
RELEASE_TARGETS := \
out/release/tildefriends \
out/winrelease/tildefriends.exe \
out/iosrelease/tildefriends \
out/iossimrelease/tildefriends \
out/macosrelease/tildefriends \
out/androidrelease/tildefriends \
out/androidrelease-armv7a/tildefriends \
out/androidrelease-x86_64/tildefriends \
out/androidrelease-x86/tildefriends \
out/armrelease/tildefriends
out/armrelease/tildefriends \
out/macosrelease-arm/tildefriends \
out/macosrelease-x86_64/tildefriends
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
ifneq ($(UNAME_S),OpenBSD)
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
endif
@ -210,8 +244,7 @@ $(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \
-DNDEBUG \
-flto
-DNDEBUG
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(ANDROID_RELEASE_TARGETS): LDFLAGS += -Oz
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -Os
@ -222,25 +255,29 @@ $(WINDOWS_TARGETS): CFLAGS += \
-D_WIN32_WINNT=0x0A00 \
-DWINVER=0x0A00 \
-DNTDDI_VERSION=NTDDI_WIN10 \
-Ideps/openssl/mingw64/usr/local/include
-Iout/openssl/$(UNAME_S)/mingw64/usr/local/include
$(WINDOWS_TARGETS): LDFLAGS += \
-static \
-lm \
-Ldeps/openssl/mingw64/usr/local/lib
-Lout/openssl/$(UNAME_S)/mingw64/usr/local/lib
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
$(AARCH64_TARGETS): AS = $(CC)
$(AARCH64_TARGETS): CFLAGS += -Ideps/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Ldeps/openssl/Linux/aarch64/usr/local/lib
$(AARCH64_TARGETS): CFLAGS += -Iout/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Lout/openssl/Linux/aarch64/usr/local/lib
ifeq ($(UNAME_S),Darwin)
$(MACOS_TARGETS): CC = xcrun clang
$(HOST_TARGETS): CC = xcrun clang
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
$(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
else ifeq ($(UNAME_S),Linux)
$(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64
$(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64 -DTARGET_OS_IPHONE=1
$(IOS_TARGETS): LDFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/ios_toolchain/target/lib deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH deps/macos_toolchain/bin/o64-clang
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH deps/macos_toolchain/bin/oa64-clang
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH deps/macos_toolchain/bin/o64-clang
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH deps/macos_toolchain/bin/oa64-clang
endif
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
@ -251,30 +288,39 @@ $(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-Wno-unknown-warning-option
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Iout/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Lout/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Iout/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Lout/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Iout/openssl/android/x86/usr/local/include
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
$(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
$(ANDROID_X86_TARGETS): LDFLAGS += -Lout/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Iout/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Lout/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
$(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0
ifeq ($(UNAME_S),Darwin)
$(IOS_TARGETS): CFLAGS += -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOS_TARGETS): CFLAGS += -Iout/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/ios/ios64-xcrun/usr/local/lib
else
$(IOS_TARGETS): CFLAGS += -Ideps/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
$(IOS_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
endif
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
$(LINUX_TARGETS) $(MACOS_TARGETS): CFLAGS += -Ideps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(LINUX_TARGETS) $(MACOS_TARGETS): LDFLAGS += -Ldeps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
$(IOSSIM_TARGETS): CFLAGS += -Iout/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Lout/openssl/ios/iossimulator-xcrun/usr/local/lib
$(HOST_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(HOST_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux)
@ -293,13 +339,14 @@ endif
get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86)))))
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease macosdebug-arm macosrelease-arm macosdebug-x86_64 macosrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86))))) \
$(if $(findstring Darwin,$(UNAME_S)),$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos)))))) \
$(if $(findstring Darwin,$(UNAME_S)),,$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))))
APP_SOURCES := $(wildcard src/*.c)
APP_SOURCES_ios := $(wildcard src/*.m)
@ -321,10 +368,12 @@ $(APP_OBJS): CFLAGS += \
-Ideps/valgrind \
-Wdouble-promotion \
-Werror
ifneq ($(UNAME_S),Darwin)
ifeq ($(UNAME_M),x86_64)
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/ios% $(BUILD_DIR)/macos%,$(APP_OBJS)): CFLAGS += \
-fanalyzer
endif
endif
ARES_SOURCES := \
deps/c-ares/src/lib/ares_addrinfo2hostent.c \
@ -561,7 +610,6 @@ $(UV_OBJS): CFLAGS += \
-Wno-unused-result \
-Wno-unused-variable \
-Wno-nonnull
$(UV_OBJS): CFLAGS += -fno-lto
$(filter out/win%,$(UV_OBJS)): \
CFLAGS += \
-Wno-cast-function-type \
@ -758,12 +806,12 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \
-pthread \
-lm
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS): LDFLAGS += \
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS) $(MACOS_TARGETS): LDFLAGS += \
-lssl \
-lcrypto
ifneq ($(UNAME_S),Haiku)
ifneq ($(UNAME_S),OpenBSD)
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-ldl
endif
endif
@ -796,27 +844,15 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
##
## Common targets:
##
debug: ## Build a debug executable for the current platform.
release: ## Build a release executable for the current platform.
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
debug: ## Build a debug executable for the current host platform.
release: ## Build a release executable for the current host platform.
armdebug: ## Cross-compile aarch64 debug on Linux.
armrelease: ## Cross-compile aarch64 release on Linux.
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
unix: debug release ## Build all UNIX targets.
win: windebug winrelease ## Build all Windows targets.
windebug: ## Cross-compile a debug win32 executable on Linux.
winrelease: ## Cross-compile a release win32 executable on Linux.
.PHONY: all win unix
##
## Windows targets:
##
windebug: ## Build a debug win32 executable.
winrelease: ## Build a release win32 executable.
##
## MacOS targets:
##
macosdebug: ## Build a MacOS debug executable.
macosrelease: ## Build a MacOS release executable.
ALL_APP_OBJS := \
$(APP_OBJS) \
$(ARES_OBJS) \
@ -988,7 +1024,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@cp -r deps/codemirror/ out/aab/staging/root/deps/
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) $@
aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab
@ -1091,14 +1127,21 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@cp -v $< $@
out/data.zip: $(RAW_FILES)
@echo [zip] $@
@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/zsign_build/zsign: $(wildcard deps/zsign/*.cpp deps/zsign/*.h deps/zsign/*.txt deps/zsign/common/*)
@+echo [cmake] $@
@cmake -B out/zsign_build deps/zsign
@cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip $(if $(HAVE_LINUX_IOS),out/zsign_build/zsign)
@mkdir -p $(dir $@)
@cp -v $< $@
@cp -v $(filter-out out/zsign%,$<) $@
@cp -v out/data.zip $(@D)/
ifeq ($(HAVE_LINUX_IOS),1)
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
@mkdir -p $(realpath $(dir $@))/_CodeSignature
@out/zsign_build/zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
endif
.SECONDARY:
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@ -1136,26 +1179,34 @@ iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install,
xcrun simctl launch booted com.unprompted.tildefriends
.PHONY: iossimdebuggo
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
ANDROID_DEPS := out/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
+@export ANDROID_NDK_ROOT=$(ANDROID_NDK)
+@export BUILD_PLATFORM=android
+@export TOOLCHAIN=$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64
+@PATH="$$TOOLCHAIN/x86_64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86_64 SSL_TARGET=android-x86_64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/i686-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86 SSL_TARGET=android-x86 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/arm-linux-androideabi/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=armeabi-v7a SSL_TARGET=android-arm OPTIONS="--target=armv7a-linux-androideabi -Wl,--fix-cortex-a8 -D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/aarch64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=arm64-v8a SSL_TARGET=android-arm64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(UNAME_S),Linux)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
ifneq ($(USE_SYSTEM_SSL),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS=-flto tools/ssl-local
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
LOCAL_DEPS := out/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu- -flto" BUILD_TARGET=aarch64 tools/ssl-local
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu-" BUILD_TARGET=aarch64 SSL_TARGET=linux-aarch64 tools/ssl-local
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_IOS),1)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
LOCAL_DEPS := out/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=deps/ios_toolchain/target/bin:$$PATH \
BUILD_TARGET=ios64-cross \
@ -1168,29 +1219,58 @@ $(LOCAL_DEPS):
tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-arm/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-arm \
SSL_TARGET=darwin64-arm64 \
CC=../../deps/macos_toolchain/bin/oa64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/arm64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-arm/% $(BUILD_DIR)/macosdebug-arm/%,$(APP_OBJS)): | $(LOCAL_DEPS)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-x86_64 \
SSL_TARGET=darwin64-x86_64 \
CC=../../deps/macos_toolchain/bin/o64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-x86_64/% $(BUILD_DIR)/macosdebug-x86_64/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
endif
ifeq ($(UNAME_S),Darwin)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS=-flto tools/ssl-local
$(filter $(BUILD_DIR)/macosdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/macosrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
WINDOWS_DEPS := out/openssl/$(UNAME_S)/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@tools/ssl-mingw64
+@BUILD_TARGET=mingw64 SSL_TARGET=mingw64 OPTIONS="--cross-compile-prefix=x86_64-w64-mingw32-" tools/ssl-local
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
endif
ifeq ($(UNAME_S),Darwin)
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
IOS_DEPS := out/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@tools/ssl-ios
+@BUILD_PLATFORM=ios BUILD_TARGET=ios64-xcrun SSL_TARGET=ios64-xcrun OPTIONS="-fPIC -Wno-macro-redefined -miphoneos-version-min=9.0" tools/ssl-local
+@BUILD_PLATFORM=ios BUILD_TARGET=iossimulator-xcrun SSL_TARGET=iossimulator-xcrun OPTIONS="-fPIC -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
@echo [lipo] $@
@deps/macos_toolchain/bin/lipo -create -output $@ $^
##
## Linux package targets:
##
@ -1218,7 +1298,8 @@ appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
.PHONY: appimage
flatpak: out/ ## Build a flatpak.
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak-builder --user --disable-rofiles-fuse --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
.PHONY: flatpak
@ -1258,7 +1339,6 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
--exclude=deps/libsodium/test \
--exclude=deps/libuv/docs \
--exclude=deps/libuv/test \
--exclude=deps/openssl \
--exclude=deps/speedscope/*.map \
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
@ -1269,7 +1349,20 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
.PHONY: tarball
dist: ## Build versions of all distributables for release.
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone $(if $(HAVE_CROSS_AARCH64), out/armrelease/tildefriends.standalone)
dist: release-apk aab out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone
ifeq ($(HAVE_LINUX_IOS),1)
dist: iosrelease-ipa
endif
ifeq ($(HAVE_LINUX_MACOS),1)
dist: out/macosrelease/tildefriends.standalone
endif
ifeq ($(HAVE_WIN),1)
dist: out/winrelease/tildefriends.standalone.exe
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
dist: out/armrelease/tildefriends.standalone
endif
dist:
@mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@ -1277,8 +1370,10 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@test $(HAVE_LINUX_IOS) && echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
@test $(HAVE_LINUX_IOS) && cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@test $(HAVE_LINUX_MACOS) && echo "[cp] tildefriends-macos-$(VERSION_NUMBER)"
@test $(HAVE_LINUX_MACOS) && cp out/macosrelease/tildefriends.standalone dist/tildefriends-macos-$(VERSION_NUMBER)
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"

View File

@ -1,18 +1,16 @@
# Tilde Friends
Tilde Friends is a tool for making and sharing.
Tilde Friends participates in the Secure Scuttlebutt decentralized social
network while also functioning as a platform for making, sharing, and running
web applications.
A public instance lives at https://www.tildefriends.net/.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, as well as a platform for writing and running web applications.
## Goals
1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a
browser.
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town.
1. Make it easy to make, share, and run all sorts of applications while
respecting the privacy and safety of your data.
## Getting the Source
@ -40,8 +38,7 @@ dependencies in the right places.
### Requirements
On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
System OpenSSL libraries are assumed to be available on Haiku and OpenSSL.
On MacOS, Xcode's command-line tools are expected to be available.
@ -63,13 +60,12 @@ repository root as the current working directory. `tildefriends -h` lists
further options.
The first user to create an account and log in will be granted administrative
privileges. Further administration can be done at
privileges. Further administration can be done in the `admin` app at
<http://localhost:12345/~core/admin/>.
## Documentation
Docs are a work in progress:
<https://dev.tildefriends.net/cory/tildefriends/wiki>.
Docs live here: <https://docs.tildefriends.net/>.
## License

View File

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

View File

@ -19,10 +19,6 @@ async function fetch_info(apps) {
return result;
}
/**
*
*
*/
async function fetch_shared_apps() {
let messages = {};
@ -69,17 +65,17 @@ async function main() {
const stylesheet = `
body {
color: whitesmoke;
font-family: sans-serif;
margin: 16px;
margin: 8px;
}
.container {
.iconbox {
display: grid;
grid-template-columns: repeat(auto-fill, 64px);
gap: 1em;
justify-content: space-around;
background-color: #ffffff10;
border: 2px solid #073642;
border-radius: 8px;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
}
.iconbox::after {
content: "";
flex: auto;
}
.app {
@ -101,16 +97,28 @@ async function main() {
`;
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 id="apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<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 id="shared_apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<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 id="core_apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<header class="w3-container w3-light-blue">
<h2>Core Apps</h2>
</header>
<div id="core_apps" class="w3-indigo iconbox"></div>
</div>
`;
const script = `
@ -126,9 +134,13 @@ async function main() {
// For each app in the provided list
for (let app of Object.keys(apps).sort()) {
// 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');
// The app's icon
@ -161,12 +173,13 @@ async function main() {
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="w3.css"></link>
<style>
${stylesheet}
</style>
</head>
<body>
<body class="w3-darkgray">
${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",
"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 || {};
let contacts = await query(
`
SELECT content FROM messages
SELECT json(content) AS content FROM messages
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
@ -189,50 +189,6 @@ async function fetch_about(db, ids, 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) {
let size = 0;
await ssb.sqlAsync(

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&bBoMW+AjErDfa483Mg3+h1L25xfDDeVSpcfD9WAwL3U=.sha256"
"previous": "&0ZtxnihH3oETfi0vhhEwc9O66SrjfiFcHDVBAIxICy0=.sha256"
}

View File

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

View File

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

View File

@ -162,7 +162,7 @@ export async function picker(callback, anchor, author) {
<div class="w3-modal-content w3-card-4">
<div
class="w3-content w3-theme-d1"
style="display: flex; flex-direction: column; max-height: 50vh"
style="display: flex; flex-direction: column; max-height: 80vh"
>
<header class="w3-container" style="flex: 0 0">
<h1>Choose a Reaction</h1>

View File

@ -18,6 +18,9 @@ class TfElement extends LitElement {
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
guest: {type: Boolean},
url: {type: String},
private_messages: {type: Array},
};
}
@ -36,8 +39,8 @@ class TfElement extends LitElement {
this.channels = [];
this.channels_unread = {};
this.channels_latest = {};
this.loading_channels_latest = 0;
this.loading_channels_latest_scheduled = 0;
this.loading_latest = 0;
this.loading_latest_scheduled = 0;
tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || [];
});
@ -66,7 +69,9 @@ class TfElement extends LitElement {
async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
this.ids = ids;
await this.load_channels();
}
@ -143,7 +148,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;
let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache;
@ -158,8 +164,8 @@ class TfElement extends LitElement {
let max_row_id = (
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;
@ -172,7 +178,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query(
`
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
messages,
json_each(?1) AS following
@ -183,7 +189,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about'
UNION
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
messages,
json_each(?2) AS following
@ -221,7 +227,11 @@ class TfElement extends LitElement {
}
users = users || {};
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);
}
@ -244,7 +254,7 @@ class TfElement extends LitElement {
this.load_channels();
}
}
this.schedule_load_channels_latest();
this.schedule_load_latest();
}
async _handle_whoami_changed(event) {
@ -259,144 +269,202 @@ 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) {
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 = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest;
const k_chunk_count = 256;
while (latest - k_chunk_count >= 0) {
let ranges = [];
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(
`
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
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.rowid > ?2 AND
messages.rowid <= ?3 AND
messages.rowid > ?1 AND
messages.rowid <= ?2 AND
json(messages.content) LIKE '"%'
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);
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, cache.messages];
}
async load_channels_latest(following) {
this.loading_channels_latest++;
try {
let start_time = new Date();
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
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel
UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`,
[
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
]
);
this.channels_latest = Object.fromEntries(
channels.map((x) => [x.channel, x.rowid])
);
console.log('latest', this.channels_latest);
console.log('unread', this.channels_unread);
console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this;
latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest,
});
console.log('private took', (new Date() - start_time) / 1000.0);
let start_time = new Date();
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
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel
UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`,
[
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
]
);
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;
start_time = new Date();
latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0],
});
} finally {
this.loading_channels_latest--;
}
console.log('private took', (new Date() - start_time) / 1000.0);
console.log(latest);
self.private_messages = latest[1];
});
}
_schedule_load_channels_latest_timer() {
--this.loading_channels_latest_scheduled;
this.schedule_load_channels_latest();
_schedule_load_latest_timer() {
--this.loading_latest_scheduled;
this.schedule_load_latest();
}
schedule_load_channels_latest() {
if (!this.loading_channels_latest) {
schedule_load_latest() {
if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load_channels_latest(this.following);
} else if (!this.loading_channels_latest_scheduled) {
this.loading_channels_latest_scheduled++;
setTimeout(this._schedule_load_channels_latest_timer.bind(this), 5000);
this.load();
} else if (!this.loading_latest_scheduled) {
this.loading_latest_scheduled++;
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() {
let start_time = new Date();
let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
blocked: v.ib,
};
by_count.push({count: v.of, id: id});
this.loading_latest = true;
try {
let start_time = new Date();
let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
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);
let about_start_time = new Date();
users = await this.fetch_about(following, users);
console.log(
'about took',
(new Date() - about_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) {
@ -448,6 +516,7 @@ class TfElement extends LitElement {
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread}
.connections=${this.connections}
.private_messages=${this.private_messages}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@ -521,7 +590,9 @@ class TfElement extends LitElement {
style="position: static; top: 0; z-index: 10"
>
<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}
>
@ -544,8 +615,20 @@ class TfElement extends LitElement {
)}
</div>
`;
let contents =
!this.loaded || this.loading
let contents = this.guest
? 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
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
>
@ -559,7 +642,7 @@ class TfElement extends LitElement {
class="w3-theme-dark"
>
<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}
</div>
</div>

View File

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

View File

@ -11,7 +11,6 @@ class TfProfileElement extends LitElement {
id: {type: String},
users: {type: Object},
size: {type: Number},
server_follows_me: {type: Boolean},
following: {type: Boolean},
blocking: {type: Boolean},
};
@ -27,7 +26,6 @@ class TfProfileElement extends LitElement {
this.id = null;
this.users = {};
this.size = 0;
this.server_follows_me = undefined;
}
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) {
tfrpc.rpc
.appendMessage(
@ -175,31 +153,11 @@ class TfProfileElement extends LitElement {
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() {
navigator.clipboard.writeText(this.id);
}
render() {
if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load();
}
this.load();
let self = this;
let profile = this.users[this.id] || {};
@ -216,22 +174,6 @@ class TfProfileElement extends LitElement {
let block;
if (this.id === this.whoami) {
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`
<button
id="save_profile"
@ -243,7 +185,6 @@ class TfProfileElement extends LitElement {
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
${server_follow}
`;
} else {
edit = html`<button
@ -276,20 +217,18 @@ class TfProfileElement extends LitElement {
let edit_profile = this.editing
? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div class="w3-container">
<div>
<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}))}></input>
</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}))}>${this.editing.description}</textarea>
<div>
<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>
</div>
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
<div>
<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>
</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>
<div>
<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>
</div>
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>`
: 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>
</header>
<div class="w3-container">
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
<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">
${edit_profile}
<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>
</div>

View File

@ -396,9 +396,9 @@ function is_dark(hex, value) {
function generated() {
let now = new Date();
let k_color = rgb_to_hex([
now.getDay() * 255 / 6,
now.getHours() * 255 / 23,
now.getSeconds() * 255 / 59,
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
]);
//let k_color = '#034f84';
//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`
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
${connection.connected
? html`
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
`
: undefined}
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined
@ -264,24 +268,27 @@ class TfTabConnectionsElement extends LitElement {
)}
</ul>
<h2>Local Accounts</h2>
<ul class="w3-ul w3-border">
<div class="w3-container">
${this.identities.map(
(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
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
>🖥 local server</span
>`
? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
🖥 local server
</div>`
: undefined}
${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
>😎 you</span
>`
? html`<div class="w3-tag w3-medium w3-round w3-theme-d1">
😎 you
</div>`
: undefined}
<tf-user id=${x} .users=${this.users}></tf-user>
</li>`
</div>`
)}
</ul>
</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 {styles} from './tf-styles.js';
@ -17,6 +17,7 @@ class TfTabNewsFeedElement extends LitElement {
loading: {type: Number},
time_range: {type: Array},
time_loading: {type: Array},
private_messages: {type: Array},
};
}
@ -57,15 +58,14 @@ class TfTabNewsFeedElement extends LitElement {
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.author != ?1 AND
messages.timestamp >= ?3 AND
messages.timestamp < ?4
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
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
JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT * FROM mentions
SELECT TRUE AS is_primary, * FROM mentions
`,
[
'"' + this.whoami.replace('"', '""') + '"',
@ -77,33 +77,29 @@ class TfTabNewsFeedElement extends LitElement {
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?
ORDER BY sequence DESC)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref
WITH
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence 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 selected
JOIN messages_refs ON selected.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
UNION
SELECT * FROM mine
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
SELECT TRUE AS is_primary, * FROM selected
`,
[this.hash.substring(1), start_time, end_time]
);
} else if (this.hash.startsWith('#%')) {
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
WHERE id = ?1
WHERE messages.id = ?1
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
ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1
@ -111,112 +107,98 @@ class TfTabNewsFeedElement extends LitElement {
[this.hash.substring(1)]
);
} else if (this.hash.startsWith('##')) {
let promises = [];
const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(
tfrpc.rpc.query(
`
WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE
messages.timestamp >= ? AND
messages.timestamp < ? AND
messages.content ->> 'channel' = ?
ORDER BY messages.timestamp DESC)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
)
);
}
result = [].concat(...(await Promise.all(promises)));
result = await tfrpc.rpc.query(
`
WITH
all_news AS (
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.content ->> 'channel' = ?4
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4),
news AS (SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT TRUE AS is_primary, news.* FROM news
`,
[
JSON.stringify(this.following),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
);
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.sequence DESC LIMIT 20
`,
[JSON.stringify(this.following), start_time, end_time]
[JSON.stringify(this.private_messages), start_time, end_time]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else {
let promises = [];
const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(
tfrpc.rpc.query(
`
WITH news AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp >= ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
start_time,
end_time,
]
)
);
}
result = [].concat(...(await Promise.all(promises)));
result = await tfrpc.rpc.query(
`
WITH
all_news AS (
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE timestamp >= 0 AND timestamp < ?3),
news AS (
SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY timestamp DESC LIMIT 20
)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT TRUE AS is_primary, news.* FROM news
`,
[JSON.stringify(this.following), start_time, end_time]
);
}
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
let only_primary = messages.filter((x) => x.is_primary);
this.time_range = [
messages.reduce(
only_primary.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
messages.reduce(
only_primary.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1]
),
@ -228,17 +210,11 @@ class TfTabNewsFeedElement extends LitElement {
this.loading_canceled = false;
try {
let more = [];
while (!more.length && !this.loading_canceled) {
let last_start_time = this.start_time;
this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(this.start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) =>
x.timestamp >= this.start_time && x.timestamp < last_start_time
)
);
}
let last_start_time = this.time_range[0];
more = await this.fetch_messages(null, last_start_time);
this.update_time_range_from_messages(
more.filter((x) => x.timestamp < last_start_time)
);
this.messages = await this.decrypt([...more, ...this.messages]);
} finally {
this.loading--;
@ -274,80 +250,69 @@ class TfTabNewsFeedElement extends LitElement {
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() {
this.loading++;
let now = new Date().valueOf();
let end_time = now + 24 * 60 * 60 * 1000;
let messages = [];
try {
messages = await this.fetch_messages(
this.time_range[1] - 24 * 60 * 60 * 1000,
end_time
);
messages = await this.fetch_messages(this.time_range[0], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[1] && x.timestamp < end_time
(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = Object.values(
Object.fromEntries(
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
this.messages = this.merge_messages(
this.messages,
Object.values(
Object.fromEntries(
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
)
);
console.log('done loading latest messages.');
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
try {
this.messages = [];
this._messages_hash = this.hash;
if (this._messages_hash !== this.hash) {
this.messages = [];
this._messages_hash = this.hash;
}
this._messages_following = this.following;
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(
this.time_range[0],
this.time_range[1]
);
this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(null, this.time_range[1]);
this.update_time_range_from_messages(
messages.filter(
(x) =>
x.timestamp >= this.time_range[0] &&
x.timestamp < this.time_range[1]
)
messages.filter((x) => x.timestamp < this.time_range[1])
);
messages = await this.decrypt(messages);
if (!messages.length) {
let more = [];
while (!more.length && start_time >= 0) {
let last_start_time = start_time;
start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) => x.timestamp >= start_time && x.timestamp < last_start_time
)
);
}
messages = await this.decrypt([...more, ...this.messages]);
}
} finally {
this.loading--;
}
this.messages = messages;
this.messages = this.merge_messages(this.messages, messages);
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() {
@ -373,7 +338,8 @@ class TfTabNewsFeedElement extends LitElement {
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following
JSON.stringify(this._messages_following) !==
JSON.stringify(this.following)
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
@ -417,7 +383,7 @@ class TfTabNewsFeedElement extends LitElement {
</p>
`;
}
return html`
return cache(html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
@ -433,7 +399,7 @@ class TfTabNewsFeedElement extends LitElement {
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news>
${more}
`;
`);
}
}

View File

@ -1,4 +1,11 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import {
LitElement,
cache,
keyed,
html,
unsafeHTML,
until,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -16,6 +23,7 @@ class TfTabNewsElement extends LitElement {
channels_unread: {type: Object},
channels_latest: {type: Object},
connections: {type: Array},
private_messages: {type: Array},
};
}
@ -128,6 +136,26 @@ class TfTabNewsElement extends LitElement {
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() {
return html`
<div
@ -153,7 +181,7 @@ class TfTabNewsElement extends LitElement {
>
`
: undefined}
<div class="w3-bar-item w3-theme-d2">Channels</div>
<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
<a
href="#"
class="w3-bar-item w3-button"
@ -194,11 +222,31 @@ class TfTabNewsElement extends LitElement {
>
`
)}
<div class="w3-bar-item w3-theme-d2">Connections</div>
${this.connections.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">Connections</h4>
${this.connections
.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
class="w3-overlay"
@ -211,12 +259,15 @@ class TfTabNewsElement extends LitElement {
render() {
let profile =
this.hash.startsWith('#@') && this.hash != '#@'
? html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
? keyed(
this.hash.substring(1),
html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
)
: undefined;
let edit_profile;
if (
@ -231,10 +282,10 @@ class TfTabNewsElement extends LitElement {
name.
</div>`;
}
return html`
return cache(html`
${this.render_sidebar()}
<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"
class="w3-main"
>
@ -287,10 +338,11 @@ class TfTabNewsElement extends LitElement {
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
.private_messages=${this.private_messages}
></tf-tab-news-feed>
</div>
</div>
`;
`);
}
}

View File

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

View File

@ -19,27 +19,34 @@ class TfUserElement extends LitElement {
}
render() {
let user = this.users[this.id];
let shape =
!user?.follow_depth || user.follow_depth >= 2 ? 'w3-circle' : 'w3-round';
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"
>?</span
>😎</span
>`;
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]) {
let image_link = this.users[this.id].image;
if (user) {
let image_link = user.image;
image_link =
typeof image_link == 'string' ? image_link : image_link?.link;
if (image_link !== undefined) {
image = html`<img
class="w3-circle"
class=${'w3-theme-l4 ' + shape}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
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}
</div>`;
}

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&7gFmLW5zSMhmxWWY1+jeRcHdullgujSqGJg94lVgr1k=.sha256"
"previous": "&wAb7J6E35xEXpiXsQ6t1RaWTGIvlatUnyH8ipF6pVic=.sha256"
}

View File

@ -10,17 +10,6 @@
<link rel="stylesheet" href="brands.min.css" />
<style>
body,
h1,
h2,
h3,
h4,
h5 {
font-family: 'Poppins', sans-serif;
}
body {
font-size: 16px;
}
img {
margin-bottom: -8px;
}
@ -39,11 +28,14 @@
<b>😎 Tilde Friends</b>
</h1>
<h1 class="w3-xxlarge w3-text-green">
<b>Make apps and friends from the comfort of your web browser.</b>
<b
>the Secure Scuttlebutt decentralized social network client that's
<i>fancy🎩</i></b
>
</h1>
<p>
Tilde Friends is a platform for building, running, and sharing web
applications.
In addition to participating in Secure Scuttlebutt, Tilde Friends is
a platform for building, running, and sharing applications.
</p>
<p>
Available for lots of devices:
@ -60,7 +52,7 @@
>
<a
class="w3-button w3-black w3-padding-large"
href="https://www.tildefriends.net/~cory/apps/"
href="https://www.tildefriends.net/~core/ssb/"
><i class="fa fa-link"></i> Try It</a
>
<a
@ -68,6 +60,11 @@
href="https://dev.tildefriends.net/"
><i class="fa fa-mug-hot"></i> Development</a
>
<a
class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/"
><i class="fa fa-book"></i> Documentation</a
>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
@ -223,16 +220,15 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Boring Technology</b></h1>
<h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1>
<p>
Tilde Friends is built using boring, trusted tech. Unless a better
reason presents itself, it strives to use only simple and widely adopted
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
Tilde Friends strives to use only simple and widely adopted dependencies
in order to keep it easy to build for all sorts of platforms and
maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits.
whatever fits on top.
</p>
<div class="w3-row" style="margin-top: 64px">

View File

@ -81,9 +81,8 @@ App.prototype.send = function (message) {
* TODOC
* @param {*} request
* @param {*} response
* @param {*} client
*/
async function socket(request, response, client) {
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
let credentials = await httpd.auth_query(request.headers);
@ -245,6 +244,6 @@ async function socket(request, response, client) {
};
response.upgrade(100, {});
}
};
export {socket, App};
export {App};

View File

@ -149,7 +149,7 @@ class TfNavigationElement extends LitElement {
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
<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%"
id="identity"
@click=${self.toggle_id_dropdown}

View File

@ -1,5 +1,4 @@
import * as app from './app.js';
import * as form from './form.js';
import * as http from './http.js';
let gProcesses = {};
@ -577,19 +576,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) {
if (
process.credentials &&
@ -848,59 +834,4 @@ exports.callAppHandler = async function callAppHandler(
response.end(answer?.data);
};
/**
* TODOC
*/
loadSettings()
.then(function (settings) {
if (tildefriends.https_port && settings.http_redirect) {
httpd.set_http_redirect(settings.http_redirect);
}
httpd.all('/app/socket', app.socket);
let port = httpd.start(tildefriends.http_port);
if (tildefriends.args.out_http_port_file) {
print('Writing the port file.');
File.writeFile(
tildefriends.args.out_http_port_file,
port.toString() + '\n'
)
.then(function (r) {
print(
'Wrote the port file:',
tildefriends.args.out_http_port_file,
r
);
})
.catch(function () {
print('Failed to write the port file.');
});
}
if (tildefriends.https_port) {
async function start_tls() {
const kCertificatePath = 'data/httpd/certificate.pem';
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
let privateKey;
let certificate;
try {
privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
certificate = utf8Decode(await File.readFile(kCertificatePath));
} catch (e) {
print(`TLS disabled (${e.message}).`);
return;
}
let context = new TlsContext();
context.setPrivateKey(privateKey);
context.setCertificate(certificate);
httpd.start(tildefriends.https_port, context);
}
start_tls();
}
})
.catch(function (error) {
print('Failed to load settings.');
printError({print: print}, error);
exit(1);
});
export {invoke, getProcessBlob};

View File

@ -1,44 +0,0 @@
/**
* TODOC
* @param {*} encoded
* @returns
*/
function decode(encoded) {
let result = '';
for (let i = 0; i < encoded.length; i++) {
let c = encoded[i];
if (c == '+') {
result += ' ';
} else if (c == '%') {
result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
i += 2;
} else {
result += c;
}
}
return result;
}
/**
* TODOC
* @param {*} encoded
* @param {*} initial
* @returns
*/
function decodeForm(encoded, initial) {
let result = initial || {};
if (encoded) {
encoded = encoded.trim();
let items = encoded.split('&');
for (let i = 0; i < items.length; i++) {
let item = items[i];
let equals = item.indexOf('=');
let key = decode(item.slice(0, equals));
let value = decode(item.slice(equals + 1));
result[key] = value;
}
}
return result;
}
export {decodeForm};

View File

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

View File

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

View File

@ -21,14 +21,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.26";
version = "0.0.27.1";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-XJ7M++risfsRn9GkS1zjTQpqqV5S09uyimeVzU9hGGg=";
hash = "sha256-3t1m9ZomQF3DteWyALJWrnCq0EAROEK8shKXh6Ao38c=";
fetchSubmodules = true;
};
@ -46,7 +46,7 @@ pkgs.stdenv.mkDerivation rec {
];
buildPhase = ''
make -j $NIX_BUILD_CORES release
make -j $NIX_BUILD_CORES release USE_SYSTEM_SSL=1
'';
installPhase = ''

File diff suppressed because one or more lines are too long

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

@ -22,7 +22,6 @@
"version": "6.18.4",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz",
"integrity": "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@ -31,10 +30,9 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz",
"integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==",
"license": "MIT",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
@ -46,7 +44,6 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@ -59,7 +56,6 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@ -76,7 +72,6 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@ -91,17 +86,15 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.10.7",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.7.tgz",
"integrity": "sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==",
"license": "MIT",
"version": "6.10.8",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz",
"integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@ -115,7 +108,6 @@
"version": "6.8.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
@ -126,7 +118,6 @@
"version": "6.5.8",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.8.tgz",
"integrity": "sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@ -134,10 +125,9 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"license": "MIT",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@ -146,7 +136,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@ -155,10 +144,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.36.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.1.tgz",
"integrity": "sha512-miD1nyT4m4uopZaDdO2uXU/LLHliKNYL9kB1C1wJHrunHLm/rpkb5QVSokqgw9hFqEZakrdlb/VGWX8aYZTslQ==",
"license": "MIT",
"version": "6.36.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.2.tgz",
"integrity": "sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==",
"dependencies": {
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
@ -170,7 +158,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -185,7 +172,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@ -195,7 +181,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@ -205,7 +190,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@ -215,15 +199,13 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -232,14 +214,12 @@
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
},
"node_modules/@lezer/css": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"license": "MIT",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.10.tgz",
"integrity": "sha512-V5/89eDapjeAkWPBpWEfQjZ1Hag3aYUUJOL8213X0dFRuXJ4BXa5NKl9USzOnaLod4AOpmVCkduir2oKwZYZtg==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -250,7 +230,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@ -259,7 +238,6 @@
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -270,7 +248,6 @@
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@ -278,10 +255,9 @@
}
},
"node_modules/@lezer/json": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
"license": "MIT",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -292,7 +268,6 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@ -300,14 +275,12 @@
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@ -332,7 +305,6 @@
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"serialize-javascript": "^6.0.1",
"smob": "^1.0.0",
@ -354,7 +326,6 @@
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@ -373,247 +344,228 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
"integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.4.tgz",
"integrity": "sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
"integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.4.tgz",
"integrity": "sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
"integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.4.tgz",
"integrity": "sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
"integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.4.tgz",
"integrity": "sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
"integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.4.tgz",
"integrity": "sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
"integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.4.tgz",
"integrity": "sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
"integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.4.tgz",
"integrity": "sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
"integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.4.tgz",
"integrity": "sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
"integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz",
"integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
"integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.4.tgz",
"integrity": "sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
"integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.4.tgz",
"integrity": "sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
"integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.4.tgz",
"integrity": "sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
"integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.4.tgz",
"integrity": "sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
"integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.4.tgz",
"integrity": "sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
"integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz",
"integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
"integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.4.tgz",
"integrity": "sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
"integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.4.tgz",
"integrity": "sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
"integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.4.tgz",
"integrity": "sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
"integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.4.tgz",
"integrity": "sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -622,21 +574,18 @@
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -648,14 +597,12 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@ -670,20 +617,17 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@ -691,15 +635,13 @@
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -712,7 +654,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -721,7 +662,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@ -733,7 +673,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
@ -747,20 +686,17 @@
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
@ -773,7 +709,6 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
@ -782,7 +717,6 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
@ -799,10 +733,9 @@
}
},
"node_modules/rollup": {
"version": "4.29.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
"integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
"license": "MIT",
"version": "4.34.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.4.tgz",
"integrity": "sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==",
"dependencies": {
"@types/estree": "1.0.6"
},
@ -814,25 +747,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.29.1",
"@rollup/rollup-android-arm64": "4.29.1",
"@rollup/rollup-darwin-arm64": "4.29.1",
"@rollup/rollup-darwin-x64": "4.29.1",
"@rollup/rollup-freebsd-arm64": "4.29.1",
"@rollup/rollup-freebsd-x64": "4.29.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
"@rollup/rollup-linux-arm-musleabihf": "4.29.1",
"@rollup/rollup-linux-arm64-gnu": "4.29.1",
"@rollup/rollup-linux-arm64-musl": "4.29.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
"@rollup/rollup-linux-riscv64-gnu": "4.29.1",
"@rollup/rollup-linux-s390x-gnu": "4.29.1",
"@rollup/rollup-linux-x64-gnu": "4.29.1",
"@rollup/rollup-linux-x64-musl": "4.29.1",
"@rollup/rollup-win32-arm64-msvc": "4.29.1",
"@rollup/rollup-win32-ia32-msvc": "4.29.1",
"@rollup/rollup-win32-x64-msvc": "4.29.1",
"@rollup/rollup-android-arm-eabi": "4.34.4",
"@rollup/rollup-android-arm64": "4.34.4",
"@rollup/rollup-darwin-arm64": "4.34.4",
"@rollup/rollup-darwin-x64": "4.34.4",
"@rollup/rollup-freebsd-arm64": "4.34.4",
"@rollup/rollup-freebsd-x64": "4.34.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.4",
"@rollup/rollup-linux-arm-musleabihf": "4.34.4",
"@rollup/rollup-linux-arm64-gnu": "4.34.4",
"@rollup/rollup-linux-arm64-musl": "4.34.4",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.4",
"@rollup/rollup-linux-riscv64-gnu": "4.34.4",
"@rollup/rollup-linux-s390x-gnu": "4.34.4",
"@rollup/rollup-linux-x64-gnu": "4.34.4",
"@rollup/rollup-linux-x64-musl": "4.34.4",
"@rollup/rollup-win32-arm64-msvc": "4.34.4",
"@rollup/rollup-win32-ia32-msvc": "4.34.4",
"@rollup/rollup-win32-x64-msvc": "4.34.4",
"fsevents": "~2.3.2"
}
},
@ -854,15 +787,13 @@
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
]
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
@ -871,15 +802,13 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -889,7 +818,6 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -898,14 +826,12 @@
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@ -914,11 +840,10 @@
}
},
"node_modules/terser": {
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"version": "5.38.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.38.0.tgz",
"integrity": "sha512-a4GD5R1TjEeuCT6ZRiYMHmIf7okbCPEuhQET8bczV6FrQMMlFXA1n+G0KKjdlFCm3TEHV77GxfZB3vZSUQGFpg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -935,8 +860,7 @@
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
}
}
}

1
deps/ios_toolchain vendored Symbolic link
View File

@ -0,0 +1 @@
/opt/deps/ios_toolchain/

2
deps/libbacktrace vendored

@ -1 +1 @@
Subproject commit d48f84034ce3e53e501d10593710d025cb1121db
Subproject commit 78af4ffa26e15532847c1ba854ece7b3bacc6b1a

2
deps/libuv vendored

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

1
deps/macos_toolchain vendored Symbolic link
View File

@ -0,0 +1 @@
/opt/deps/macos_toolchain/

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>
</body></html>
<!DOCTYPE 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
Sat Nov 16 22:13:27 PST 2024
d36c3a54424063a8df7bc67a7b824a223d73861b
speedscope@1.22.0
Thu Jan 16 16:49:47 PST 2025
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

640
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()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.47.2"
#define SQLITE_VERSION_NUMBER 3047002
#define SQLITE_SOURCE_ID "2024-12-07 20:39:59 2aabe05e2e8cae4847a802ee2daddc1d7413d8fc560254d93ee3e72c14685b6c"
#define SQLITE_VERSION "3.48.0"
#define SQLITE_VERSION_NUMBER 3048000
#define SQLITE_SOURCE_ID "2025-01-14 11:05:00 d2fe6b05f38d9d7cd78c5d252e99ac59f1aea071d669830c1ffe4e8966e84010"
/*
** 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
** 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]]
** 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
@ -1253,6 +1258,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_EXTERNAL_READER 40
#define SQLITE_FCNTL_CKSM_FILE 41
#define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43
/* deprecated names */
#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
** statement on the database connection specified by the only parameter.
** 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
** the return value of sqlite3_changes() is undefined. ^Executing any other
** 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
** 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
** to return an error (error code SQLITE_ERROR) if the statement uses
** 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>
*/
#define SQLITE_PREPARE_PERSISTENT 0x01
#define SQLITE_PREPARE_NORMALIZE 0x02
#define SQLITE_PREPARE_NO_VTAB 0x04
#define SQLITE_PREPARE_DONT_LOG 0x10
/*
** CAPI3REF: Compiling An SQL Statement
@ -10889,7 +10910,7 @@ SQLITE_API int sqlite3_deserialize(
#ifdef __cplusplus
} /* End of the 'extern "C"' block */
#endif
#endif /* SQLITE3_H */
/* #endif for SQLITE3_H will be added by mksqlite3.tcl */
/******** Begin file sqlite3rtree.h *********/
/*
@ -13140,14 +13161,29 @@ struct Fts5PhraseIter {
** value returned by xInstCount(), SQLITE_RANGE is returned. Otherwise,
** output variable (*ppToken) is set to point to a buffer containing the
** matching document token, and (*pnToken) to the size of that buffer in
** bytes. This API is not available if the specified token matches a
** prefix query term. In that case both output variables are always set
** to 0.
** bytes.
**
** 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
** 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
** "detail=none" or "detail=column" option.
**
@ -13581,3 +13617,4 @@ struct fts5_api {
#endif /* _FTS5_H */
/******** End of fts5.h *********/
#endif /* SQLITE3_H */

1
deps/zsign vendored Submodule

@ -0,0 +1 @@
Subproject commit d995d539ff16f28d985f5b2a1c62dd4eb9f029ea

1
docs

@ -1 +0,0 @@
Subproject commit a40758cc4bef5ded4d214c2acf1c95e554e20564

View File

@ -0,0 +1,63 @@
# App Development Cheat Sheet
Making apps for the impatient tilde friend.
## Prerequisites
- either run your own instance or use [tildefriends.net](https://www.tildefriends.net/)
- register and login
- [optional] use the `ssb` app to create yourself an SSB identity
## Development Process
1. hit the `edit` link from any app or new app URL
2. make sure the path in the text box is under your username: `/~username/app/`
3. write server-side code in `app.js`
4. click the `save` button or press the save hotkey (Alt+S or _[browser-specific modifiers]_+S)
5. see the app reload on the right side
## Output
- `app.setDocument(html)` - send HTML to the browser
- `print(...)` - send values to the browser's developer console
## Persistence
- `app.localStorageGet(key)` -> `value`
- `app.localStorageSet(key, value)`
- `database()`, `shared_database(key)`, `my_shared_database(package, key)`
- `db.get(key)` -> `value`
- `db.set(key, value)`
- `db.exchange(key, expected, value)` -> `exchanged`
- `db.remove(key)`
- `db.getAll()` -> `[key1, ...]`
- `db.getLike(pattern)` -> `{key1: value1, ...}`
## SSB
- `ssb.createIdentity()` -> `id`
- `ssb.getIdentities()` -> `[id1, ...]`
- `ssb.appendMessageWithIdentity(id, content)` -> `message_id`
- `ssb.blobStore(blob)` -> `blob_id`
- `ssb.blobGet(id)` -> `blob`
- `ssb.sqlAsync(query, args, row_callback)`
## TF-RPC
Stock helper code for calling functions across the web server and browser boundary.
- on the server: `import * as tfrpc from "/tfrpc.js";`
- in the browser: `import * as tfrpc from "/static/tfrpc.js";`
- either direction:
- register a function: `tfrpc.register(function my_function() {});`
- call a remote function: `let promise = tfrpc.rpc.my_function();`
## Share
- give out web links: [https://www.tildefriends.net/~cory/screwble/](https://www.tildefriends.net/~cory/screwble/)
- use the `Attach App` button when composing a post in [the SSB app](https://www.tildefriends.net/~core/ssb/)
## More Docs
- [api reference](https://www.tildefriends.net/~cory/api/)
- [source code](https://dev.tildefriends.net/cory/tildefriends/releases)

View File

@ -0,0 +1,166 @@
# App Development Guide
A Tilde Friends application starts with code that runs on a Tilde Friends server, possibly far away from where you wrote it, in a little JavaScript environment, in its own restricted process, with the only access to the outside world being the ability to send messages to the server. This document gives some recipes showing how that can be used to build a functional user-facing application in light of the unique constraints present.
## Example 1: Hello, world!
Of course we must start with a classic.
### app.js
```js
app.setDocument('<h1 style="color: #fff">Hello, world!</h1>');
```
### Output
<h1 style="color: #fff">Hello, world!</h1>
### Explanation
At a glance, this might seem mundane, but for it to work:
- the server starts a real process for your app and loads your code into it
- your code runs
- `app.setDocument()` sends a message back to the server
- the server interprets the message and redirects it to the browser
- `core/client.js` in the browser receives the message and puts your HTML into an iframe
- your HTML is presented by the browser in an iframe sandbox
But you don't have to think about all that. Call a function, and you see the result.
## Example 2: Hit Counter
Let's take advantage of code running on the server and create a little hit counter using a key value store shared between all visitors.
### app.js
```js
async function main() {
let db = await shared_database('visitors');
let count = parseInt((await db.get('visitors')) ?? '0') + 1;
await db.set('visitors', count.toString());
await app.setDocument(`
<h1 style="color: #fff">Welcome, visitor #${count}!</h1>
`);
}
main();
```
### Output
<h1 style="color: #fff">Welcome, visitor #1!</h1>
### Explanation
Just as pure browser apps have access to `localStorage`, Tilde Friends apps have access to key-value storage on the server.
The interface is a bit clunky and will likely change someday, but this example gets a database object, from which you can get and set string values by key. There are various on `shared_database` that let you store data that is private to the user or shared by different criteria.
Also, even though any browser-side code is sandboxed, it is allowed to access browser local storage by going through Tilde Friends API, because sometimes that is useful.
## Example 3: Files
Suppose you don't want to create your entire app in a single server-side file as we've done with the previous examples. There are some tools to allow you to begin to organize.
### app.js
```js
async function main() {
let html = utf8Decode(await getFile('index.html'));
app.setDocument(html);
}
main();
```
### index.html
```html
<html>
<head>
<script type="module" src="script.js"></script>
</head>
<body style="color: #fff">
<h1>File Test</h1>
</body>
</html>
```
### script.js
```js
window.addEventListener('load', function() {
document.body.appendChild(document.createTextNode('Hello, world');
});
```
### Output
<h1>File Test</h1><p>Hello, world!</p>
### Explanation
On the server, `utf8Decode(await getFile(fileName))` lets you load a file from your app. In the browser, your app files are made available by HTTP, so you can `<script src="my_script.js"></script>` and such to access them.
## Example 4: Remote Procedure Call
While making calls between the client and the server, it is possible to pass functions across that boundary. `tfrpc.js` is a tiny script which builds on that feature to try to hide some of the complexities.
### app.js
```js
import * as tf from '/tfrpc.js';
function sum() {
let s = 0;
for (let x of arguments) {
s += x;
}
return s;
}
tf.register(sum);
async function main() {
app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
```
### index.html
```html
<html>
<body>
<h1 id="result">Calculating...</h1>
</body>
<script type="module" src="script.js"></script>
</html>
```
### script.js
```js
import * as tf from '/static/tfrpc.js';
window.addEventListener('load', async function () {
document.getElementById('result').innerText = await tf.rpc.sum(1, 2, 3);
});
```
### Output
<h1>6</h1>
### Explanation
Here the browser makes an asynchronous call to the server to do some basic math and update its DOM with the result.
With your favorite Vue/Lit/React/... library on the client-side and your favorite Tilde Friends API calls registered with tfrpc, it becomes pretty easy to start extracting interesting information from, say, SQL queries over Secure Scuttlebutt data, and generating complicated, dynamic user interface. These are the building blocks I used to make the current Tilde Friends SSB client interface.
## Conclusion
Tilde Friends is currently a pile of all the parts that I thought I needed to build interesting web applications, tied together by code that tries to walk the fine line between being secure enough to let us safely run code on the same device and being usable enough that you can open a tab in your browser and start building just by typing code.
I don't claim it thoroughly accomplishes either yet, but I believe it is at a stage where it is showing how promising this approach can be, and I am excited for you to take it for a spin and share.

15
docs/inspiration.md Normal file
View File

@ -0,0 +1,15 @@
# Inspiration
This is an ever-growing list of software that is similar to what Tilde Friends tries to be but as far as I can tell don't quite fit the same niche.
- Secure Scuttlebutt Clients
- [Manyverse](https://www.manyver.se/)
- [Patchwork](https://github.com/ssbc/patchwork)
- [Patchfox](https://patchfox.org/#/)
- [Habitat](https://gitlab.com/quickdudley/habitat)
- [Āhau](https://gitlab.com/ahau/ahau/)
- [erlbutt](https://github.com/cmoid/erlbutt/)
- Web Application Platforms
- [Glitch](https://glitch.com/)
- [Val Town](https://www.val.town/)
- [Clace](https://clace.io/)

19
docs/release_checklist.md Normal file
View File

@ -0,0 +1,19 @@
# Release Checklist
- make sure ci is passing
- run the tests
- format + prettier
- update metadata/en-US/changelogs
- git tag
- push
- make dist
- make a release on gitea
- upload the artifacts
- nix
- comment out the hash in default.nix
- update the version
- run `nix-build`
- update the hash
- bump the versions in GNUmakefile for the next release
- make
- commit

64
docs/vision.md Normal file
View File

@ -0,0 +1,64 @@
# Vision
Tilde Friends is a tool for making and sharing.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, and an environment for creating and running web applications.
## Why
This is a thing that I wanted to exist and wanted to work on. No other reason.
There is not a business model. I believe it is interesting and unique.
## Goals
1. Make it **easy and fun** to run all sorts of web applications.
2. Provide **security** that is easy to understand and protects your data.
3. Make **creating and sharing** web applications accessible to anyone with a
browser.
## Ways to Use Tilde Friends
1. **Social Network User**: This is a social network first. You are just here,
because your friends are. Or you like how we limit your message length or
short videos or whatever the trend is. If you are ambitious, you click links
and see interactive experiences (apps) that you wouldn't see elsewhere.
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
lists, wiki pages, ..., and you interact with them as though they were
cloud-hosted by a megacorporation. They just work, and you don't think twice.
3. **Group leader**: You host or use a small public instance, installing apps for
a group of friends to use as web visitors.
4. **Developer**: You like to write code and make or improve apps for fun or to
solve problems. When you encounter a Tilde Friends app on a strange server,
you know you can trivially modify it or download it to your own instance.
## Future Goals / Endgame
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
the web interface publicly, but you can sync, install and edit apps, and
otherwise get the full experience from a tiny touch screen.
2. The universal application runtime. The web browser is the universal
platform, but even for the simplest application that you might want to host
for your friends, cloud hosting, containers, and complicated dependencies might
all enter the mix. Tilde Friends, though it is yet another thing to host,
includes everything you need out of the box to run a vast variety of interesting
apps.
Tilde Friends will be built out, gradually providing safe access to host
resources and client resources the same way web browsers extended access to
resources like GPU, persistent storage, cameras, ... over the years.
Not much effort has been put forward yet to having a robust, long-lasting API,
but since the client side longevity is already handled by web browsers, it
seems possible that the server-side API can be managed in a similar way.
3. An awesome development environment. Right now it runs JavaScript from the
first embeddable text editor I could poorly configure enough to edit code,
but it could incorporate a debugger, source control integration a la ssb-git,
merge tools, and transpiling from all sorts of different languages.

View File

@ -0,0 +1,20 @@
* 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/.
* Disconnect reasons.
* Improved unread status.
* Improved replication.
* Show messages as they arrive.
* Loading screen.
* Faster load.
* 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",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},

View File

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

View File

@ -1,6 +1,6 @@
id: com.unprompted.tildefriends
runtime: org.freedesktop.Platform
runtime-version: '23.08'
runtime-version: '24.08'
sdk: org.freedesktop.Sdk
command: tildefriends-run.sh
finish-args:

View File

@ -18,20 +18,6 @@
static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_read_file_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_stat(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_stat_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static double _time_spec_to_double(const uv_timespec_t* time_spec);
static void _file_on_stat_complete(uv_fs_t* request);
typedef struct file_stat_t
{
void* _task;
JSContext* _context;
promiseid_t _promise;
uv_fs_t _request;
} file_stat_t;
typedef struct fs_req_t
{
@ -45,12 +31,10 @@ void tf_file_register(JSContext* context)
{
JSValue global = JS_GetGlobalObject(context);
JSValue file = JS_NewObject(context);
void* task = JS_GetContextOpaque(context);
tf_task_t* task = JS_GetContextOpaque(context);
const char* zip = tf_task_get_zip_path(task);
JS_SetPropertyStr(context, global, "File", file);
JS_SetPropertyStr(context, file, "readFile", JS_NewCFunction(context, zip ? _file_read_file_zip : _file_read_file, "readFile", 1));
JS_SetPropertyStr(context, file, "writeFile", JS_NewCFunction(context, _file_write_file, "writeFile", 2));
JS_SetPropertyStr(context, file, "stat", JS_NewCFunction(context, zip ? _file_stat_zip : _file_stat, "stat", 1));
JS_FreeValue(context, global);
}
@ -65,105 +49,6 @@ static void _file_async_close_callback(uv_fs_t* req)
tf_free(req);
}
static void _file_write_write_callback(uv_fs_t* req)
{
uv_fs_req_cleanup(req);
fs_req_t* fsreq = (fs_req_t*)req;
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
tf_task_resolve_promise(task, promise, JS_NewInt64(context, req->result));
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to write %s: %s", req->path, uv_strerror(req->result)));
}
int result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
static void _file_write_open_callback(uv_fs_t* req)
{
fs_req_t* fsreq = (fs_req_t*)req;
const char* path = tf_strdup(req->path);
uv_fs_req_cleanup(req);
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
uv_buf_t buf = { .base = fsreq->buffer, .len = fsreq->size };
fsreq->file = req->result;
int result = uv_fs_write(req->loop, req, fsreq->file, &buf, 1, 0, _file_write_write_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to write %s: %s", path, uv_strerror(result)));
result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
tf_free(req);
}
tf_free((void*)path);
}
static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
const char* file_name = JS_ToCString(context, argv[0]);
size_t size;
uint8_t* buffer = tf_util_try_get_array_buffer(context, &size, argv[1]);
bool is_array_buffer = false;
if (buffer)
{
is_array_buffer = true;
}
else
{
buffer = (uint8_t*)JS_ToCStringLen(context, &size, argv[1]);
}
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
fs_req_t* req = tf_malloc(sizeof(fs_req_t) + size);
*req = (fs_req_t)
{
.fs =
{
.data = (void*)(intptr_t)promise,
},
.size = size,
};
memcpy(req->buffer, buffer, size);
if (!is_array_buffer)
{
JS_FreeCString(context, (const char*)buffer);
}
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, file_name, UV_FS_O_CREAT | UV_FS_O_WRONLY, 0644, _file_write_open_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for write: %s", file_name, uv_strerror(result)));
}
JS_FreeCString(context, file_name);
return promise_value;
}
static void _file_read_read_callback(uv_fs_t* req)
{
fs_req_t* fsreq = (fs_req_t*)req;
@ -224,8 +109,9 @@ static void _file_read_open_callback(uv_fs_t* req)
static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
tf_task_t* task = JS_GetContextOpaque(context);
const char* file_name = JS_ToCString(context, argv[0]);
const char* actual = tf_task_get_path_with_root(task, file_name);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
@ -239,7 +125,7 @@ static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int ar
.size = k_file_read_max,
};
memset(req + 1, 0, k_file_read_max);
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, file_name, UV_FS_O_RDONLY, 0, _file_read_open_callback);
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, actual, UV_FS_O_RDONLY, 0, _file_read_open_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for read: %s", file_name, uv_strerror(result)));
@ -247,6 +133,7 @@ static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int ar
tf_free(req);
}
JS_FreeCString(context, file_name);
tf_free((char*)actual);
return promise_value;
}
@ -365,81 +252,6 @@ static JSValue _file_read_file_zip(JSContext* context, JSValueConst this_val, in
return promise_value;
}
static JSValue _file_stat(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
const char* path = JS_ToCString(context, argv[0]);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
file_stat_t* data = tf_malloc(sizeof(file_stat_t));
data->_task = task;
data->_promise = promise;
data->_request.data = data;
data->_context = context;
int result = uv_fs_stat(tf_task_get_loop(task), &data->_request, path, _file_on_stat_complete);
if (result)
{
tf_task_reject_promise(task, promise, JS_NewInt32(context, result));
uv_fs_req_cleanup(&data->_request);
tf_free(data);
}
JS_FreeCString(context, path);
return promise_value;
}
static JSValue _file_stat_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
file_stat_t* data = tf_malloc(sizeof(file_stat_t));
data->_task = task;
data->_promise = promise;
data->_request.data = data;
data->_context = context;
/* Ignore the requested path and stat the zip itself. */
int result = uv_fs_stat(tf_task_get_loop(task), &data->_request, tf_task_get_zip_path(task), _file_on_stat_complete);
if (result)
{
tf_task_reject_promise(task, promise, JS_NewInt32(context, result));
uv_fs_req_cleanup(&data->_request);
tf_free(data);
}
return promise_value;
}
static double _time_spec_to_double(const uv_timespec_t* time_spec)
{
return time_spec->tv_sec + (double)(time_spec->tv_nsec) / 1e9;
}
static void _file_on_stat_complete(uv_fs_t* request)
{
file_stat_t* data = (file_stat_t*)(request->data);
JSContext* context = data->_context;
if (request->result)
{
tf_task_reject_promise(data->_task, data->_promise, JS_NewInt32(context, request->result));
}
else
{
JSValue result = JS_NewObject(context);
JS_SetPropertyStr(context, result, "mtime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_mtim)));
JS_SetPropertyStr(context, result, "ctime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_ctim)));
JS_SetPropertyStr(context, result, "atime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_atim)));
JS_SetPropertyStr(context, result, "size", JS_NewFloat64(context, request->statbuf.st_size));
tf_task_resolve_promise(data->_task, data->_promise, result);
JS_FreeValue(context, result);
}
uv_fs_req_cleanup(request);
tf_free(data);
}
typedef struct _stat_t
{
uv_fs_t request;

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_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));
*connection = (tf_http_connection_t) { .http = http, .tcp = { .data = connection }, .is_receiving_headers = true };
if (listener->tls)

View File

@ -7,6 +7,7 @@
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "tls.h"
#include "tlscontext.js.h"
#include "trace.h"
#include "util.js.h"
@ -233,46 +234,6 @@ static JSValue _httpd_make_response_object(JSContext* context, tf_http_request_t
return response_object;
}
static void _httpd_callback_internal(tf_http_request_t* request, bool is_websocket)
{
http_handler_data_t* data = request->user_data;
JSContext* context = data->context;
JSValue request_object = JS_NewObject(context);
JS_SetPropertyStr(context, request_object, "method", JS_NewString(context, request->method));
JS_SetPropertyStr(context, request_object, "uri", JS_NewString(context, request->path));
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
if (request->query)
{
JS_SetPropertyStr(context, request_object, "query", JS_NewString(context, request->query));
}
if (request->body)
{
JS_SetPropertyStr(context, request_object, "body", tf_util_new_uint8_array(context, request->body, request->content_length));
}
JSValue client = JS_NewObject(context);
JS_SetPropertyStr(context, client, "tls", request->is_tls ? JS_TRUE : JS_FALSE);
JS_SetPropertyStr(context, request_object, "client", client);
JSValue response_object = _httpd_make_response_object(context, request);
/* The ref is owned by the JS object and will be released by the finalizer. */
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response_object,
};
JSValue response = JS_Call(context, data->callback, JS_UNDEFINED, 2, args);
tf_util_report_error(context, response);
JS_FreeValue(context, request_object);
JS_FreeValue(context, response);
JS_FreeValue(context, response_object);
}
static bool _httpd_redirect(tf_http_request_t* request)
{
if (request->is_tls)
@ -292,16 +253,6 @@ static bool _httpd_redirect(tf_http_request_t* request)
return true;
}
static void _httpd_callback(tf_http_request_t* request)
{
if (_httpd_redirect(request))
{
return;
}
_httpd_callback_internal(request, false);
}
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_request_t* request = JS_GetOpaque(this_val, _httpd_request_class_id);
@ -388,71 +339,19 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
return JS_UNDEFINED;
}
static void _httpd_cleanup_callback(void* user_data)
{
http_handler_data_t* data = user_data;
JS_FreeValue(data->context, data->callback);
tf_free(data);
}
static JSValue _httpd_endpoint_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
const char* pattern = JS_ToCString(context, argv[0]);
http_handler_data_t* data = tf_malloc(sizeof(http_handler_data_t));
*data = (http_handler_data_t) { .context = context, .callback = JS_DupValue(context, argv[1]) };
tf_http_add_handler(http, pattern, _httpd_callback, _httpd_cleanup_callback, data);
JS_FreeCString(context, pattern);
return JS_UNDEFINED;
}
typedef struct _httpd_listener_t
{
JSContext* context;
JSValue tls;
tf_tls_context_t* tls;
} httpd_listener_t;
static void _httpd_listener_cleanup(void* user_data)
{
httpd_listener_t* listener = user_data;
JS_FreeValue(listener->context, listener->tls);
tf_free(listener);
}
static JSValue _httpd_endpoint_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
int port = 0;
JS_ToInt32(context, &port, argv[0]);
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
*listener = (httpd_listener_t) { .context = context, .tls = JS_DupValue(context, argv[1]) };
tf_tls_context_t* tls = tf_tls_context_get(listener->tls);
int assigned_port = tf_http_listen(http, port, tls, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http%s://127.0.0.1:%d/" RESET ".\n", tls ? "s" : "", assigned_port);
return JS_NewInt32(context, assigned_port);
}
static void _httpd_free_user_data(void* user_data)
{
tf_free(user_data);
}
static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_http_t* http = JS_GetOpaque(this_val, _httpd_class_id);
http_user_data_t* user_data = tf_http_get_user_data(http);
if (!user_data)
if (listener->tls)
{
user_data = tf_malloc(sizeof(http_user_data_t));
memset(user_data, 0, sizeof(http_user_data_t));
tf_http_set_user_data(http, user_data, _httpd_free_user_data);
tf_tls_context_destroy(listener->tls);
}
const char* redirect = JS_ToCString(context, argv[0]);
snprintf(user_data->redirect, sizeof(user_data->redirect), "%s", redirect ? redirect : "");
JS_FreeCString(context, redirect);
return JS_UNDEFINED;
tf_free(listener);
}
typedef struct _auth_query_work_t
@ -783,13 +682,24 @@ typedef struct _http_file_t
char etag[512];
} http_file_t;
static bool _ends_with(const char* a, const char* suffix)
{
if (!a || !suffix)
{
return false;
}
size_t alen = strlen(a);
size_t suffixlen = strlen(suffix);
return alen >= suffixlen && strcmp(a + alen - suffixlen, suffix) == 0;
}
static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
http_file_t* file = user_data;
tf_http_request_t* request = file->request;
if (result >= 0)
{
if (strcmp(path, "core/tfrpc.js") == 0)
if (strcmp(path, "core/tfrpc.js") == 0 || _ends_with(path, "core/tfrpc.js"))
{
const char* content_type = _ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
@ -931,9 +841,10 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
}
tf_task_t* task = request->user_data;
size_t size = strlen(file_path) + strlen(after) + 1;
const char* root_path = tf_task_get_root_path(task);
size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen(file_path) + strlen(after) + 1;
char* path = alloca(size);
snprintf(path, size, "%s%s", file_path, after);
snprintf(path, size, "%s%s%s%s", root_path ? root_path : "", root_path ? "/" : "", file_path, after);
tf_http_request_ref(request);
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}
@ -2255,6 +2166,100 @@ static void _httpd_endpoint_logout(tf_http_request_t* request)
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
static void _httpd_endpoint_app_socket(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
JSValue response = _httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response,
};
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}
static int _tf_httpd_get_tildefriends_int(JSContext* context, const char* arg)
{
JSValue global = JS_GetGlobalObject(context);
JSValue tildefriends = JS_GetPropertyStr(context, global, "tildefriends");
JSValue arg_value = JS_GetPropertyStr(context, tildefriends, arg);
int value = 0;
JS_ToInt32(context, &value, arg_value);
JS_FreeValue(context, arg_value);
JS_FreeValue(context, tildefriends);
JS_FreeValue(context, global);
return value;
}
static const char* _tf_httpd_get_tildefriends_arg_string(JSContext* context, const char* arg)
{
JSValue global = JS_GetGlobalObject(context);
JSValue tildefriends = JS_GetPropertyStr(context, global, "tildefriends");
JSValue args = JS_GetPropertyStr(context, tildefriends, "args");
JSValue arg_value = JS_GetPropertyStr(context, args, arg);
const char* value = !JS_IsUndefined(arg_value) ? JS_ToCString(context, arg_value) : NULL;
const char* result = value ? tf_strdup(value) : NULL;
JS_FreeCString(context, value);
JS_FreeValue(context, arg_value);
JS_FreeValue(context, args);
JS_FreeValue(context, tildefriends);
JS_FreeValue(context, global);
return result;
}
static void _httpd_free_user_data(void* user_data)
{
tf_free(user_data);
}
static const char* _httpd_read_file(tf_task_t* task, const char* path)
{
const char* actual = tf_task_get_path_with_root(task, path);
const size_t k_max_read = 8 * 1024 * 1024;
char* result = NULL;
char* buffer = tf_malloc(k_max_read);
FILE* file = fopen(actual, "rb");
if (file)
{
size_t size = fread(buffer, 1, k_max_read, file);
result = tf_malloc(size + 1);
memcpy(result, buffer, size);
result[size] = '\0';
fclose(file);
}
tf_free(buffer);
tf_free((char*)actual);
return result;
}
void tf_httpd_register(JSContext* context)
{
JS_NewClassID(&_httpd_class_id);
@ -2275,15 +2280,41 @@ void tf_httpd_register(JSContext* context)
{
fprintf(stderr, "Failed to register Request.\n");
}
int http_port = _tf_httpd_get_tildefriends_int(context, "http_port");
int https_port = _tf_httpd_get_tildefriends_int(context, "https_port");
const char* out_http_port_file = _tf_httpd_get_tildefriends_arg_string(context, "out_http_port_file");
JSValue global = JS_GetGlobalObject(context);
JSValue httpd = JS_NewObjectClass(context, _httpd_class_id);
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
uv_loop_t* loop = tf_task_get_loop(task);
tf_http_t* http = tf_http_create(loop);
tf_http_set_trace(http, tf_task_get_trace(task));
JS_SetOpaque(httpd, http);
if (https_port)
{
http_user_data_t* user_data = tf_http_get_user_data(http);
if (!user_data)
{
user_data = tf_malloc(sizeof(http_user_data_t));
memset(user_data, 0, sizeof(http_user_data_t));
tf_http_set_user_data(http, user_data, _httpd_free_user_data);
}
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_string(db, "http_redirect", user_data->redirect, sizeof(user_data->redirect));
tf_ssb_release_db_reader(ssb, db);
/* Workaround. */
if (strcmp(user_data->redirect, "0") == 0)
{
*user_data->redirect = '\0';
}
}
tf_http_add_handler(http, "/", _httpd_endpoint_root, NULL, task);
tf_http_add_handler(http, "/codemirror/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/lit/*", _httpd_endpoint_static, NULL, task);
@ -2312,10 +2343,56 @@ void tf_httpd_register(JSContext* context)
tf_http_add_handler(http, "/login/logout", _httpd_endpoint_logout, NULL, task);
tf_http_add_handler(http, "/login", _httpd_endpoint_login, NULL, task);
JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_endpoint_all, "all", 2));
JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_endpoint_start, "start", 2));
JS_SetPropertyStr(context, httpd, "set_http_redirect", JS_NewCFunction(context, _httpd_set_http_redirect, "set_http_redirect", 1));
tf_http_add_handler(http, "/app/socket", _httpd_endpoint_app_socket, NULL, task);
JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1));
JS_SetPropertyStr(context, global, "httpd", httpd);
JS_FreeValue(context, global);
if (http_port > 0 || out_http_port_file)
{
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
*listener = (httpd_listener_t) { 0 };
int assigned_port = tf_http_listen(http, http_port, NULL, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "http://127.0.0.1:%d/" RESET ".\n", assigned_port);
if (out_http_port_file)
{
const char* actual_http_port_file = tf_task_get_path_with_root(task, out_http_port_file);
FILE* file = fopen(actual_http_port_file, "wb");
if (file)
{
fprintf(file, "%d", assigned_port);
fclose(file);
tf_printf("Wrote the port file: %s.\n", out_http_port_file);
}
else
{
tf_printf("Failed to open %s for write: %s.\n", out_http_port_file, strerror(errno));
}
tf_free((char*)actual_http_port_file);
}
if (https_port)
{
const char* k_certificate = "data/httpd/certificate.pem";
const char* k_private_key = "data/httpd/privatekey.pem";
const char* certificate = _httpd_read_file(task, k_certificate);
const char* private_key = _httpd_read_file(task, k_private_key);
if (certificate && private_key)
{
tf_tls_context_t* tls = tf_tls_context_create();
tf_tls_context_set_certificate(tls, certificate);
tf_tls_context_set_private_key(tls, private_key);
httpd_listener_t* listener = tf_malloc(sizeof(httpd_listener_t));
*listener = (httpd_listener_t) { .tls = tls };
int assigned_port = tf_http_listen(http, https_port, tls, _httpd_listener_cleanup, listener);
tf_printf(CYAN "~😎 Tilde Friends" RESET " " YELLOW VERSION_NUMBER RESET " is now up at " MAGENTA "https://127.0.0.1:%d/" RESET ".\n", assigned_port);
}
tf_free((char*)certificate);
tf_free((char*)private_key);
}
}
tf_free((void*)out_http_port_file);
}

View File

@ -1 +0,0 @@
\mainpage Tilde Friends Source Documentation

View File

@ -15,6 +15,7 @@
#include "unzip.h"
#include <getopt.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
@ -42,11 +43,11 @@
struct backtrace_state* g_backtrace_state;
#if !defined(TARGET_OS_IPHONE)
#if !TARGET_OS_IPHONE
static const char* _get_db_path()
{
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)
{
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_import(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_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_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_verify(const char* file, int argc, char* argv[]);
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." },
{ "export", _tf_command_export, "Export apps from SSB." },
{ "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." },
{ "verify", _tf_command_verify, "Verify a feed." },
{ "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);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
if (optind < argc)
{
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_set_quiet(ssb, true);
if (optind < argc)
{
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);
_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 };
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;
}
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[])
{
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];
_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);
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL))
{
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;
}
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 = 1;
int expires = 3600;
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 (default: 1).\n");
tf_printf(" -e, --expires seconds How long this invite is valid in seconds (-1 for indefinitely, default: 1 hour).\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[])
{
const char* identity = NULL;
@ -692,6 +1256,47 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
snprintf(db_path_buffer, sizeof(db_path_buffer), "%s.%d", args->db_path, index);
db_path = db_path_buffer;
}
char* cwd = NULL;
if (!args->zip)
{
size_t cwd_size = 1;
uv_cwd((char[1]) { 0 }, &cwd_size);
cwd = alloca(cwd_size);
if (uv_cwd(cwd, &cwd_size) == 0)
{
size_t test_path_size = cwd_size + strlen("/core/core.js");
char* test_path = alloca(test_path_size);
uv_loop_t* loop = tf_task_get_loop(task);
uv_fs_t req = { 0 };
while (true)
{
snprintf(test_path, test_path_size, "%s/core/core.js", cwd);
int r = uv_fs_access(loop, &req, test_path, 0000, NULL);
uv_fs_req_cleanup(&req);
if (r != UV_ENOENT)
{
break;
}
char* slash = strrchr(cwd, '/');
if (slash)
{
*slash = '\0';
}
else
{
break;
}
}
tf_printf("Using %s as the working directory.\n", cwd);
}
if (!*cwd)
{
cwd = NULL;
}
}
tf_task_set_db_path(task, db_path);
tf_task_activate(task);
tf_ssb_set_verbose(tf_task_get_ssb(task), args->verbose);
@ -702,13 +1307,33 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
{
tf_ssb_import_from_zip(tf_task_get_ssb(task), args->zip, "core", "apps");
}
else if (cwd)
{
size_t apps_path_size = strlen(cwd) + strlen("/apps") + 1;
char* apps_path = alloca(apps_path_size);
snprintf(apps_path, apps_path_size, "%s/apps", cwd);
tf_ssb_import(tf_task_get_ssb(task), "core", apps_path);
}
else
{
tf_ssb_import(tf_task_get_ssb(task), "core", "apps");
}
}
tf_ssb_set_main_thread(tf_task_get_ssb(task), true);
if (tf_task_execute(task, args->script))
const char* script = args->script;
if (!script && cwd)
{
size_t script_size = (cwd ? strlen(cwd) : 0) + strlen("/core/core.js") + 1;
char* script_buffer = alloca(script_size);
snprintf(script_buffer, script_size, "%s/core/core.js", cwd);
script = script_buffer;
}
else if (!script)
{
script = "core/core.js";
}
tf_task_set_root_path(task, cwd);
if (tf_task_execute(task, script))
{
tf_task_run(task);
result = 0;
@ -792,7 +1417,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
const char* default_db_path = _get_db_path();
tf_run_args_t args = {
.count = 1,
.script = "core/core.js",
.http_port = 12345,
.https_port = 12346,
.ssb_port = 8008,
@ -1008,6 +1632,21 @@ static void _error_handler(int sig)
_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[])
{
char buffer[8] = { 0 };
@ -1041,20 +1680,24 @@ static void _startup(int argc, char* argv[])
#endif
bool use_error_handler = false;
#if defined(__ANDROID__)
#if defined(__ANDROID__) || defined(_WIN32)
use_error_handler = true;
#endif
if (use_error_handler)
{
if (
#if !defined(_WIN32)
signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR ||
signal(SIGSYS, _error_handler) == SIG_ERR ||
#endif
signal(SIGSEGV, _error_handler) == SIG_ERR)
signal(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSEGV, _error_handler) == SIG_ERR)
{
perror("signal");
}
}
#if defined(_WIN32)
AddVectoredExceptionHandler(0, _win32_exception_handler);
#endif
}
#if defined(__ANDROID__)
@ -1204,7 +1847,6 @@ void tf_run_thread_start(const char* zip_path)
tf_run_thread_data_t* data = tf_malloc(sizeof(tf_run_thread_data_t));
tf_run_args_t args = {
.count = 1,
.script = "core/core.js",
.http_port = 12345,
.https_port = 12346,
.ssb_port = 8008,

516
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];
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);
@ -114,6 +114,7 @@ static void _tf_ssb_connections_timer(uv_timer_t* timer)
tf_ssb_connections_t* connections = timer->data;
if (tf_ssb_is_shutting_down(connections->ssb))
{
uv_timer_stop(timer);
return;
}
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 &&
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);
@ -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 &&
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);
@ -215,7 +218,7 @@ static void _tf_ssb_connections_update_work(tf_ssb_t* ssb, void* user_data)
int r = sqlite3_step(statement);
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);
@ -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)
{
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)
@ -287,7 +293,7 @@ static void _tf_ssb_connections_sync_broadcast_visit(
}
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 "sodium/crypto_hash_sha256.h"
#include "sodium/crypto_sign.h"
#include "sodium/crypto_sign_ed25519.h"
#include "sodium/randombytes.h"
#include "sqlite3.h"
#include "uv.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
typedef struct _message_store_t message_store_t;
@ -106,6 +108,24 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
" flags INTEGER,"
" 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'"))
{
@ -168,6 +188,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, "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;
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
{
@ -603,11 +631,10 @@ bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_
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;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1";
if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK)
{
@ -617,7 +644,6 @@ bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id)
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
@ -979,8 +1005,9 @@ int tf_ssb_sqlite_authorizer(void* user_data, int action_code, const char* arg0,
break;
case SQLITE_READ:
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_refs_message_idx") == 0 || strcmp(arg0, "messages_refs_ref_idx") == 0 || strcmp(arg0, "sqlite_master") == 0 || false)
strcmp(arg0, "messages_stats") == 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_refs_message_idx") == 0 ||
strcmp(arg0, "messages_refs_ref_idx") == 0 || strcmp(arg0, "sqlite_master") == 0 || false)
? SQLITE_OK
: SQLITE_DENY;
break;
@ -1306,9 +1333,9 @@ static bool _is_blocked_by_active_blocks(const char* id, const block_node_t* blo
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;
}
@ -1335,7 +1362,7 @@ static following_t* _make_following_node(const char* id, following_t*** followin
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_stmt* statement = NULL;
@ -1354,7 +1381,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
if (sqlite3_column_type(statement, 1) != SQLITE_NULL)
{
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 (is_following)
@ -1376,7 +1403,7 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
if (sqlite3_column_type(statement, 2) != SQLITE_NULL)
{
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 (is_blocking)
@ -1402,13 +1429,14 @@ static void _populate_follows_and_blocks(tf_ssb_t* ssb, following_t* entry, foll
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);
if (depth < max_depth && !entry->populated && !_is_blocked_by_active_blocks(entry->id, active_blocks))
{
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)
{
@ -1417,28 +1445,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))
{
_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;
int following_count = 0;
for (int i = 0; i < count; i++)
{
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
_get_following(ssb, entry, &following, &following_count, 0, depth, 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, include_blocks);
entry->ref_count++;
}
int actual_following_count = 0;
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++;
}
@ -1450,13 +1478,14 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in
int write_index = 0;
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);
result[write_index].following_count = following[i]->following_count;
result[write_index].blocking_count = following[i]->blocking_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].depth = following[i]->depth;
write_index++;
}
}
@ -1478,8 +1507,8 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
int following_count = 0;
for (int i = 0; i < count; i++)
{
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL);
following_t* entry = _make_following_node(ids[i], &following, &following_count, NULL, false);
_get_following(ssb, entry, &following, &following_count, 0, depth, NULL, false);
entry->ref_count++;
}
@ -1902,6 +1931,12 @@ static void _tf_ssb_db_resolve_index_work(tf_ssb_t* ssb, void* user_data)
}
}
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)
@ -2030,6 +2065,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));
}
if (!result)
{
*out_value = tf_util_get_default_global_setting_bool(name);
}
return result;
}
@ -2053,6 +2092,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));
}
if (!result)
{
*out_value = tf_util_get_default_global_setting_int(name);
}
return result;
}
@ -2076,5 +2119,153 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
{
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;
}
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 > 0 AND 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))
{
char buffer[2] = { 0 };
size_t buffer_size = sizeof(buffer);
bool verbose = uv_os_getenv("TF_SSB_VERBOSE", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
if (verbose)
{
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, (expires_seconds > 0 ? (int64_t)time(NULL) : 0) + 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.
** @param ssb The SSB instasnce.
** @param db The SQLite database instance to use.
** @param id The blob identifier.
** @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.
@ -286,6 +286,8 @@ typedef struct _tf_ssb_following_t
int followed_by_count;
/** The number of known users blocking the account. */
int blocked_by_count;
/** Degree of separation between initial accounts and this account. */
int depth;
/** The account's identity. */
char id[k_id_base64_len];
} 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 count The number of identities.
** @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.
@ -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);
/**
** 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.
** @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_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
{
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;
/** An SSB instance. */
typedef struct _tf_ssb_t tf_ssb_t;
/** An SSB connection. */
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. */
typedef struct _tf_trace_t tf_trace_t;
/** 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);
/**
** 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().
** @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);
/**
** 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.
** @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);
/**
** 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.
** @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);
/**
** 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.
** @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);
/**
** 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;
}
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
{
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);
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, "connected", JS_NewBool(context, tf_ssb_connection_is_connected(connection)));
const char* destroy_reason = tf_ssb_connection_get_destroy_reason(connection);
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);
if (connection)
{
tf_ssb_connection_close(connection, "Close requested by user.");
tf_ssb_connection_close(connection, "Closed by user");
}
JS_FreeCString(context, id);
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_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_sqlAsync_start_timer);
uv_timer_init(tf_ssb_get_loop(ssb), &work->timeout);
JSValue result = JS_NewPromiseCapability(context, work->promise);
JSValue error_value = JS_UNDEFINED;
JSValue result = JS_UNDEFINED;
if (ssb)
{
result = JS_NewPromiseCapability(context, work->promise);
int32_t length = tf_util_get_length(context, argv[1]);
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);
}
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;
}
@ -1974,11 +1858,6 @@ static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, i
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])
{
if (!user || !identity)
@ -2027,14 +1906,12 @@ typedef struct _private_message_encrypt_t
{
const char* signer_user;
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;
const char* message;
size_t message_size;
JSValue promise[2];
bool error_id_not_found;
bool error_secretbox_failed;
bool error_scalarmult_failed;
char* encrypted;
size_t encrypted_length;
} 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)
{
uint8_t public_key[crypto_box_PUBLICKEYBYTES] = { 0 };
uint8_t secret_key[crypto_box_SECRETKEYBYTES] = { 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);
work->encrypted = tf_ssb_private_message_encrypt(private_key, work->recipients, work->recipient_count, work->message, work->message_size);
work->encrypted_length = work->encrypted ? strlen(work->encrypted) : 0;
}
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;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->error_secretbox_failed)
if (!work->encrypted)
{
result = JS_ThrowInternalError(context, "crypto_secretbox_easy failed");
}
else if (work->error_scalarmult_failed)
{
result = JS_ThrowInternalError(context, "crypto_scalarmult failed");
result = JS_ThrowInternalError(context, "Encrypt failed.");
}
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);
}
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);
JS_FreeValue(context, result);
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);
}
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++)
{
JSValue recipient = JS_GetPropertyUint32(context, argv[2], i);
const char* id = JS_ToCString(context, recipient);
if (id)
{
const char* type = strstr(id, ".ed25519");
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");
}
recipients[i] = tf_strdup(id);
JS_FreeCString(context, id);
}
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)
{
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)
@ -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, "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, "d", JS_NewInt32(context, following->out_following[i].depth));
JS_SetPropertyStr(context, object, following->out_following[i].id, entry);
}
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, "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, "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, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));

View File

@ -3,6 +3,7 @@
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.ebt.h"
#include "ssb.h"
#include "util.js.h"
@ -19,30 +20,9 @@ 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_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 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_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data);
static void _tf_ssb_rpc_connection_blobs_createWants_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);
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)
@ -161,7 +141,9 @@ static void _tf_ssb_rpc_blobs_has_work(tf_ssb_connection_t* connection, void* us
{
blobs_has_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
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)
@ -297,6 +279,7 @@ static void _tf_ssb_rpc_blobs_createWants(
tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, "blobs.createWants");
return;
}
tf_ssb_connection_add_request(connection, -request_number, "blobs.createWants", _tf_ssb_rpc_connection_blobs_createWants_callback, NULL, NULL, NULL);
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
tf_ssb_add_blob_want_added_callback(ssb, _tf_ssb_rpc_blob_wants_added_callback, NULL, connection);
blob_wants->request_number = request_number;
@ -315,8 +298,28 @@ static void _tf_ssb_rpc_tunnel_callback(tf_ssb_connection_t* connection, uint8_t
if (flags & k_ssb_rpc_flag_end_error)
{
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];
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);
}
else
@ -435,6 +438,35 @@ static void _tf_ssb_rpc_room_meta(tf_ssb_connection_t* connection, uint8_t flags
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_endpoint_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)
{
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
@ -460,6 +492,7 @@ static void _tf_ssb_rpc_room_attendants(tf_ssb_connection_t* connection, uint8_t
tf_ssb_connection_t* connections[1024];
int count = tf_ssb_get_connections(ssb, connections, tf_countof(connections));
bool have_endpoints = false;
for (int i = 0; i < count; i++)
{
char id[k_id_base64_len] = { 0 };
@ -469,15 +502,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);
}
if (tf_ssb_connection_is_endpoint(connections[i]))
{
have_endpoints = true;
}
}
JS_SetPropertyStr(context, state, "ids", ids);
tf_ssb_connection_rpc_send_json(connection, flags, -request_number, NULL, state, NULL, NULL, NULL);
JS_FreeValue(context, joined);
JS_FreeValue(context, state);
if (have_endpoints)
{
_tf_ssb_rpc_send_endpoints(ssb);
}
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
{
char id[k_blob_id_len];
@ -879,6 +935,7 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
break;
}
}
tf_ssb_ebt_set_messages_sent(tf_ssb_connection_get_ebt(connection), request->author, request->out_max_sequence_seen);
if (!request->out_finished)
{
_tf_ssb_connection_send_history_stream(
@ -908,7 +965,7 @@ static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t*
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)
{
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));
*async = (tf_ssb_connection_send_history_stream_t) {
@ -967,197 +1024,20 @@ static void _tf_ssb_rpc_createHistoryStream(
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];
int64_t value;
} ebt_clock_row_t;
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++)
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
tf_ssb_ebt_clock_t* clock = tf_ssb_ebt_get_messages_to_send(ebt);
if (clock)
{
int64_t sequence = 0;
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))
for (int i = 0; i < clock->count; i++)
{
int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
JS_SetPropertyStr(context, full_clock, id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
tf_ssb_ebt_clock_entry_t* entry = &clock->entries[i];
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, entry->id, entry->value, false, live, false);
}
JS_FreeValue(context, in_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);
tf_free(clock);
}
}
@ -1171,17 +1051,54 @@ typedef struct _resend_clock_t
{
tf_ssb_connection_t* connection;
int32_t request_number;
int pending;
} 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)
{
resend_clock_t* resend = user_data;
if (!skip)
{
_tf_ssb_rpc_ebt_replicate_send_clock(resend->connection, resend->request_number, JS_UNDEFINED);
tf_ssb_connection_set_sent_clock(resend->connection, true);
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
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)
@ -1204,33 +1121,38 @@ static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t f
JSValue name = JS_GetPropertyStr(context, args, "name");
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))
{
/* Looks like a message. */
tf_ssb_connection_adjust_read_backpressure(connection, 1);
tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection);
if (tf_ssb_connection_get_sent_clock(connection) && !tf_ssb_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 = (resend_clock_t) {
.connection = connection,
.request_number = request_number,
.pending = pending,
};
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, author);
}
@ -1284,6 +1206,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);
}
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)
{
JSContext* context = tf_ssb_get_context(ssb);
@ -1305,6 +1343,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_get_flags(connection) & k_tf_ssb_connect_flag_use_invite)
{
_tf_ssb_rpc_send_invite_use(connection);
}
JSValue message = JS_NewObject(context);
JSValue name = JS_NewArray(context);
JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel"));
@ -1347,6 +1390,11 @@ static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_chang
}
}
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 +1517,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)
{
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;
}
int64_t start_ns = uv_hrtime();
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_release_db_reader(ssb, db);
const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops);
@ -1682,11 +1733,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)
{
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);
@ -1698,6 +1744,110 @@ static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t
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)
{
tf_ssb_add_connections_changed_callback(ssb, _tf_ssb_rpc_connections_changed_callback, NULL, NULL);
@ -1708,9 +1858,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, "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.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.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, "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, "invite.use", _tf_ssb_rpc_invite_use, NULL, NULL); /* ASYNC */
}

View File

@ -18,6 +18,10 @@
#include "sodium/crypto_sign.h"
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if !defined(_WIN32)
#include <sys/wait.h>
#endif
@ -767,7 +771,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)
{
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));
}
@ -1221,4 +1225,175 @@ void tf_ssb_test_replicate(const tf_test_options_t* options)
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

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);
/**
** Test replication.
** Test connecting by string.
** @param options The test 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

@ -40,6 +40,10 @@
#define WEXITSTATUS(x) (x)
#endif
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#if !defined(__APPLE__) && !defined(__OpenBSD__)
#include <malloc.h>
#endif
@ -155,6 +159,7 @@ typedef struct _tf_task_t
int _https_port;
char _db_path[256];
char _zip_path[256];
char _root_path[256];
unzFile _zip;
const char* _args;
@ -363,7 +368,15 @@ static const char* _task_loadFile(tf_task_t* task, const char* fileName, size_t*
}
else
{
FILE* file = fopen(fileName, "rb");
const char* actual = fileName;
if (*task->_root_path)
{
size_t size = strlen(task->_root_path) + strlen(fileName) + 2;
char* buffer = alloca(size);
snprintf(buffer, size, "%s/%s", task->_root_path, fileName);
actual = fileName;
}
FILE* file = fopen(actual, "rb");
if (file)
{
fseek(file, 0, SEEK_END);
@ -716,7 +729,6 @@ static JSValue _tf_task_platform(JSContext* context, JSValueConst this_val, int
#elif defined(__ANDROID__)
return JS_NewString(context, "android");
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
return JS_NewString(context, "iphone");
#else
@ -1714,7 +1726,6 @@ void tf_task_activate(tf_task_t* task)
JS_SetPropertyStr(context, global, "TlsContext", tf_tls_context_register(context));
tf_file_register(context);
tf_database_register(context);
tf_httpd_register(context);
task->_ssb = tf_ssb_create(&task->_loop, task->_context, task->_db_path, task->_network_key);
tf_ssb_set_trace(task->_ssb, task->_trace);
@ -1731,10 +1742,18 @@ void tf_task_activate(tf_task_t* task)
}
JS_SetPropertyStr(context, tildefriends, "ssb_port", JS_NewInt32(context, actual_ssb_port));
JS_SetPropertyStr(context, tildefriends, "http_port", JS_NewInt32(context, task->_http_port));
JS_SetPropertyStr(context, tildefriends, "https_port", JS_NewInt32(context, task->_https_port));
if (task->_http_port)
{
JS_SetPropertyStr(context, tildefriends, "http_port", JS_NewInt32(context, task->_http_port));
}
if (task->_https_port)
{
JS_SetPropertyStr(context, tildefriends, "https_port", JS_NewInt32(context, task->_https_port));
}
JS_SetPropertyStr(context, global, "getStats", JS_NewCFunction(context, _tf_task_getStats, "getStats", 0));
tf_httpd_register(context);
}
else
{
@ -1819,6 +1838,12 @@ void tf_task_destroy(tf_task_t* task)
{
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->_exports);
task->_imports = NULL;
@ -1831,10 +1856,7 @@ void tf_task_destroy(tf_task_t* task)
while (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_unlink(task, timeout);
uv_close((uv_handle_t*)&timeout->_timer, _timeout_closed);
@ -1859,14 +1881,6 @@ void tf_task_destroy(tf_task_t* task)
tf_free(task->_promises);
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)
{
sqlite3_close(task->_db);
@ -2020,11 +2034,34 @@ void tf_task_set_zip_path(tf_task_t* task, const char* zip_path)
}
}
void tf_task_set_root_path(tf_task_t* task, const char* path)
{
snprintf(task->_root_path, sizeof(task->_root_path), "%s", path ? path : "");
}
const char* tf_task_get_zip_path(tf_task_t* task)
{
return task->_zip ? task->_zip_path : NULL;
}
const char* tf_task_get_root_path(tf_task_t* task)
{
return *task->_root_path ? task->_root_path : NULL;
}
const char* tf_task_get_path_with_root(tf_task_t* task, const char* path)
{
if (!*task->_root_path)
{
return tf_strdup(path);
}
size_t size = strlen(task->_root_path) + 1 + strlen(path) + 1;
char* result = tf_malloc(size);
snprintf(result, size, "%s/%s", task->_root_path, path);
return result;
}
void tf_task_set_args(tf_task_t* task, const char* args)
{
task->_args = args;

View File

@ -112,12 +112,34 @@ void tf_task_set_db_path(tf_task_t* task, const char* path);
void tf_task_set_zip_path(tf_task_t* task, const char* path);
/**
** Get the path to the zipp file being used for static data.
** Set the path to the root of the project directory for data.
** @param task The task.
** @param path The file path or NULL.
*/
void tf_task_set_root_path(tf_task_t* task, const char* path);
/**
** Get the path to the zip file being used for static data.
** @param task The task.
** @return The zip file path or NULL.
*/
const char* tf_task_get_zip_path(tf_task_t* task);
/**
** Get the path to use for reading loose files.
** @param task The task.
** @return The path or NULL.
*/
const char* tf_task_get_root_path(tf_task_t* task);
/**
** Get the path to use for reading a given loose file.
** @param task The task.
** @param path The path to the file.
** @return The path or NULL. Free with tf_free().
*/
const char* tf_task_get_path_with_root(tf_task_t* task, const char* path);
/**
** Set arbitrary named arguments that will be made available to the task.
** @param task The task.

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, "publish", tf_ssb_test_publish, 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");
#endif
}

View File

@ -34,6 +34,7 @@ typedef struct _tf_trace_stack_t
typedef struct _tf_trace_thread_t
{
pthread_t id;
int index;
tf_trace_stack_t* stack;
char name[64];
} 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_trace_thread_t) {
.id = self,
.index = trace->threads_count,
};
#if defined(__linux__) && !defined(__ANDROID__)
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;
}
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);
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->tags[stack->count] = tag;
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_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];
stack->count--;
}
*out_thread = thread;
return name;
}
@ -265,11 +269,11 @@ static void _tf_trace_begin_tagged(tf_trace_t* trace, const char* name, void* ta
return;
}
pthread_t self = pthread_self();
_tf_push_stack(trace, self, name, tag);
tf_trace_thread_t* thread = NULL;
_tf_push_stack(trace, pthread_self(), name, tag, &thread);
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 += snprintf(line + p, sizeof(line) - p, "\"},");
p = tf_min(p, tf_countof(line));
@ -288,15 +292,15 @@ static void _tf_trace_end_tagged(tf_trace_t* trace, void* tag)
return;
}
pthread_t self = pthread_self();
const char* name = _tf_pop_stack(trace, self, tag);
tf_trace_thread_t* thread = NULL;
const char* name = _tf_pop_stack(trace, pthread_self(), tag, &thread);
if (!name)
{
return;
}
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 += snprintf(line + p, sizeof(line) - p, "\"},");
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++)
{
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(),
(uint64_t)thread->id, thread->name);
size += snprintf(buffer + size, k_buffer_size - size, "{\"ph\":\"M\",\"pid\":%d,\"tid\":%d,\"name\":\"thread_name\",\"args\":{\"name\":\"%s\"}},\n", getpid(),
thread->index, thread->name);
}
uv_rwlock_rdunlock(&trace->threads_lock);
if (begin)

View File

@ -20,6 +20,16 @@
#include <execinfo.h>
#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)
{
size_t length = 0;
@ -304,69 +314,139 @@ static JSValue _util_parseHttpResponse(JSContext* context, JSValueConst this_val
return result;
}
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
static bool _is_mobile()
typedef enum _value_kind_t
{
#if defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
return true;
#else
return false;
#endif
k_kind_bool,
k_kind_int,
k_kind_string,
} value_kind_t;
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)
{
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);
for (int i = 0; i < tf_countof(k_settings); i++)
{
JSValue entry = JS_NewObject(context);
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, "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);
}
return settings;

View File

@ -10,6 +10,9 @@
#include <stdbool.h>
/** An event loop. */
typedef struct uv_loop_s uv_loop_t;
/**
** Register utility script functions.
** @param context The JS context.
@ -150,6 +153,19 @@ const char* tf_util_function_to_string(void* function);
_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.
** @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))))
/**
** 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);
/** @} */

View File

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

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)
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-auth', 'shadow_root', '#register_label'], ('click',))
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', '#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-auth', 'shadow_root', '#register_label'], ('click',))
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', '=edit'], ('click',))
driver.get('http://localhost:8888')
driver.get('http://localhost:8888/~core/apps/')
select(driver, ['#document', 'frame', '=identity'])
@ -170,7 +170,7 @@ try:
id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
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', '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',))

Some files were not shown because too many files have changed in this diff Show More