Compare commits

..

302 Commits

Author SHA1 Message Date
e5f58c2898 Produce user info for the server identity for admin users.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m52s
2024-09-19 12:22:38 -04:00
f83863ef01 This doc was ancient, so paste some of the latest from the wiki in. It's something.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m18s
2024-09-18 20:16:35 -04:00
837f069cf5 Update CodeMirror. 2024-09-18 20:16:35 -04:00
9f057dc29a Add F-Droid and c-ares to the welcome page.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-18 20:01:36 -04:00
c4904f176c Distinguish the server identity in the identity app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m46s
2024-09-18 19:09:44 -04:00
d3a5aba703 A brave new world where admin users can use the server identity.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m31s
2024-09-17 12:47:28 -04:00
9e283e427c Fix viewing apps by blob ID URL.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m18s
2024-09-16 12:45:06 -04:00
133ba31d66 c-ares 1.33.1. 2024-09-15 08:57:22 -04:00
241a65a92a sus. Disable a warning in c-ares during ltcg with gcc 13 on Haiku.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m2s
2024-09-15 12:52:28 -04:00
0b54795bab Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m29s
2024-09-11 20:28:06 -04:00
6208193de5 Fix plumbing for replies on the search tab.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 20:25:55 -04:00
c53321532f Update CodeMirror. 2024-09-11 20:18:57 -04:00
34f25e3e06 How did I not have an index on type? Wow.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 19:53:07 -04:00
c46244366e I don't know what GHES is, but it says to use this version, so sure.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m50s
2024-09-11 19:52:43 -04:00
6518af04fc It finally built. Now let's upload some artifacts.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 16m15s
2024-09-11 19:33:37 -04:00
bf137ff1f7 Seriously? More volume mount config.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m30s
2024-09-11 19:00:30 -04:00
1877955b62 Syntax?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 18:54:39 -04:00
50d0875de2 Mount volumes across another container?
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-09-11 18:49:06 -04:00
bf151e6b7d I am confused.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 18:41:55 -04:00
82893402d0 Maybe we can sign here?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 17m55s
2024-09-10 21:54:36 -04:00
8049102787 More silent OpenSSL build for mingw.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 19m24s
2024-09-10 21:19:15 -04:00
f42cc3d9fd Find the Android SDKs you just installed.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-10 21:01:20 -04:00
5f9a5208db Appease the sdkmanager version number whatever.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m56s
2024-09-10 20:34:25 -04:00
6df506d238 Spelling.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m7s
2024-09-10 20:25:51 -04:00
2bd3354256 yaml better??
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 2m32s
2024-09-10 20:22:06 -04:00
b55aaa1d18 Maybe CI android?? 2024-09-10 20:20:40 -04:00
34e19505bd No longer need this test. 2024-09-09 15:57:43 -04:00
6e06ec0904 Clean up connections that don't handshake in time.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 11m26s
2024-09-09 15:25:10 -04:00
a5814074fe OpenSSL 3.3.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m26s
2024-09-04 20:24:32 -04:00
d7479df5a2 Latest CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m23s
2024-09-04 20:11:28 -04:00
34508aa0ae dist slightly more in parallel. Exclude dotfiles from data.zip.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-04 20:07:26 -04:00
ae096b2c9c Try harder to make webview localStorage work on different versions. I suspect that's what #73 is about.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m43s
2024-09-04 12:50:12 -04:00
95d036e34a Build an AppImage. Why not?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m27s
2024-08-28 20:55:52 -04:00
4af5e8ec42 I guess prettier says this, now.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m30s
2024-08-28 20:24:26 -04:00
2a5f71bd5d This now lives in the fdroiddata repository.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m26s
2024-08-28 19:59:49 -04:00
97fb63dda1 Actually 0.0.23-wip. 2024-08-28 19:59:34 -04:00
87d42e3b3b 0.0.23-wip again. Let's gooooo.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m45s
2024-08-28 19:49:36 -04:00
0394129a4c nix => 0.0.22 again.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-28 19:42:31 -04:00
3c499c834b Fix stale data being saved when setting global settings.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 8m53s
2024-08-28 19:39:05 -04:00
17d6cc7d46 Let's try 0.0.22 again.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m47s
2024-08-28 19:20:55 -04:00
646bd7dc38 Fix changing boolean settings. 2024-08-28 19:16:19 -04:00
56e483782d Let's start work on 0.0.23. Clean out some libuv non-submodule cruft while I'm in here. 2024-08-28 19:10:16 -04:00
e1b9066b26 nix => 0.0.22.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 8m56s
2024-08-28 18:49:35 -04:00
7114ce2516 Let's release 0.0.22.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m54s
2024-08-28 18:40:10 -04:00
9240c6570a Changelog updates. Almost ready for a release.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m48s
2024-08-26 12:27:46 -04:00
f80a44ccd7 Title case settings names in the admin app.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-26 12:24:36 -04:00
e6f5eb244e Missing port.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m6s
2024-08-25 22:00:35 -04:00
ab62e83110 Fixed some peer ID brokenness.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-25 21:56:01 -04:00
aeefb9e536 Configure c-ares for haiku a bit better.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-25 13:48:57 -04:00
ee0efa536a Fix and assert against some more unsafe cross-thread JSContext use.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m10s
2024-08-25 13:30:46 -04:00
2523130fdc Fix some weird layout in the admin app on mobile.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-25 13:03:19 -04:00
c024777184 #buildfix
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m58s
2024-08-25 12:45:42 -04:00
5951d7cd2d Kill some warnings.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m41s
2024-08-25 10:07:44 -04:00
011670c70b Pass along and use the actual port we're listening on for peers.exchange.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m5s
2024-08-25 09:50:28 -04:00
6cebd6c769 Try to be more static.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m49s
2024-08-25 09:39:05 -04:00
546ae5cbf1 Latest CodeMirror. 2024-08-24 21:57:13 -04:00
f543cc642e Clean up some error'd RPC requests. Don't send blobs.createWants if we're not replicating.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m59s
2024-08-24 10:39:47 -04:00
8ac3c5ea22 Keep c-ares initialized. Fixes android, which can't just be re-initialized.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m48s
2024-08-22 12:43:20 -04:00
63918f0680 Make blobs.has do its work off the main thread so it doesn't violate that assert, and make the test cover such things a bit better.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m48s
2024-08-21 22:55:40 -04:00
bfb3d8b8a2 Add an option to disable account registation, and fix use of a JSContext from the wrong thread along the way.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 20:56:21 -04:00
e38ff99607 Special treatment to make TXT record lookup work on android.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 20:27:43 -04:00
b0e3d922c8 libuv busy loop in uv__run_timers with -flto. Sigh.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 19:40:07 -04:00
a15bb8e994 Don't rely on being idle to do anything. Fixes JS job starvation on slow machines more.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 12:53:38 -04:00
6f487100cd Format. 2024-08-20 12:35:42 -04:00
0693a2315f Fix async job starvation if everything is running too slowly.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-20 12:26:34 -04:00
f360e886ff Make -t peer_exchange complete and test that something happened.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-19 12:29:40 -04:00
6ea08cc5dc Add the beginnings of a peers.exchange test and begin to fix fallout.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m3s
2024-08-15 12:48:24 -04:00
347c706d6f ci: undefined reference to arc4random_buf
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m11s
2024-08-15 12:12:58 -04:00
5f5e6616c7 Install graphviz for building docs.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m1s
2024-08-14 21:16:31 -04:00
657bcadc7e Work-in-progress, untested, naive peer exchange. Intended to be disabled by default by a setting.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m56s
2024-08-14 21:07:16 -04:00
107666cc60 Add a setting to toggle whether replication is allowed, to be able to make a pure room, or even less, node. 2024-08-14 20:02:46 -04:00
b37669184a doxygen -u # 1.9.8 2024-08-14 20:01:21 -04:00
163a01f224 sqlite 3.46.1. 2024-08-14 19:43:57 -04:00
3d58094199 Fix some sanitizer issues, and disable LTO in debug builds to save some iteration time. 2024-08-14 19:40:20 -04:00
463951a4f1 Track/show the origin of each broadcast (discovery/room/peer exchange). 2024-08-14 19:23:01 -04:00
34804d5162 Fix android crashing in c-ares and a makefile typo.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-14 18:55:34 -04:00
3895c33915 Implement prompt() for android. #72
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-14 12:45:22 -04:00
17f4eb1a56 Make it easier to copy ids from the profile view.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-11 16:26:24 -04:00
0abdffdea6 Fix OpenBSD.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-11 11:17:49 -04:00
d32999f178 Decouple DNS-based seed discovery from the broadcast timer.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-08 18:50:54 -04:00
f621feb843 Fix some builds and make the windows build actually succeed at resolving what I want.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m13s
2024-08-07 22:25:38 -04:00
8d277f029d Support using a seeds host for bootstrapping connections. 2024-08-07 21:03:39 -04:00
1788a02338 Add c-ares. These are the hoops I have to jump through to be able to provide some bootstrap nodes. 2024-08-07 20:21:39 -04:00
ba0800d16c Lit 3.2.0. 2024-08-06 12:19:10 -04:00
4008c7d8f6 Latest CodeMirror. 2024-08-06 12:18:54 -04:00
610a2e2afc Latest libbacktrace. 2024-08-06 12:18:32 -04:00
6f3715d1eb Latest libsodium stable. 2024-08-06 12:18:21 -04:00
b78ecaa814 F-Droid looks all set for now. Let's start 0.0.22. 2024-08-06 12:17:26 -04:00
e6f5399d53 Clear out timestamp and file modes on classes.dex, too.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m21s
2024-08-05 12:43:22 -04:00
0e5806cadd Re-add classes.dex to the F-Droid APK. (!)
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m55s
2024-08-05 12:26:10 -04:00
68c9d4afa7 Found some docs that say the icon.png max size is 512x512.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m23s
2024-08-04 21:54:42 -04:00
f0ea38fe49 Just set SOURCE_DATE_EPOCH=1. Using the last commit time is complicated (have to rebuild OpenSSL every commit/release). This only affects a debug string that we don't expose.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m51s
2024-08-04 12:54:02 -04:00
b0332f923e Debugging a SOURCE_DATE_EPOCH thing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m58s
2024-08-04 12:16:46 -04:00
8a76c25394 Silence some OpenSSL build output so I can see what else is going on. Also install the signed fdroid APK in dist.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-04 12:10:52 -04:00
fd96126e3e Ooh, can I just exclude OpenSSL submodules? I don't want to see those.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m51s
2024-08-04 11:49:04 -04:00
ff3fbedc18 Fix inconsistent file modes in zip.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m4s
2024-08-04 11:25:06 -04:00
8791419f8e Sort better, and actually use ndk r26d.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m8s
2024-08-04 10:21:04 -04:00
5447b247a0 Back to r26d, and pin the timezome to get SOURCE_DATE_EPOCH to work correctly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-04 09:54:33 -04:00
aabbb10564 for fdroid: Use android ndk r27, set SOURCE_DATE_EPOCH for the android ssl build, and remove a non-determinism in AndroidManifest.xml.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m50s
2024-08-04 09:36:46 -04:00
3ccd6c9a3e I missed.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m3s
2024-08-02 22:26:01 -04:00
c290240de7 Make a release to make sure F-Droid can pick it up.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-02 22:20:18 -04:00
8e799b174b Address some fdroid zip non-determinism.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m15s
2024-08-02 21:55:00 -04:00
a9c3a93989 Add some images for F-Droid.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m1s
2024-08-02 20:37:27 -04:00
3ef8698f42 Put android:versionCode and such back in the static AndroidManifest.xml. I forgot that F-Droid needs to see it. 2024-08-02 20:37:05 -04:00
fa4e843c30 Update default.nix. Did I do it right finally?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m34s
2024-07-31 20:14:40 -04:00
9a4d11f4d9 Attempt to shrink OpenSSL on android again, ineffectively.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m14s
2024-07-31 19:58:41 -04:00
eed2b8d618 Latest CodeMirror. 2024-07-31 19:49:52 -04:00
13f02c2aca Preparing to release 0.0.21.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m33s
2024-07-31 12:50:35 -04:00
d50f8fbc8b ios: ssl fix.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m15s
2024-07-27 21:31:31 -04:00
155238a516 build: I mean -flto=auto.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m19s
2024-07-27 11:08:28 -04:00
427fcdbdca build: -flto all the things.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m34s
2024-07-25 16:02:14 -04:00
ca05d402a7 An exercise in stripping down the win32 .exe size.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m12s
2024-07-24 15:25:36 -04:00
c5a80b68ca Fixed more aab build issues. 2024-07-24 14:03:21 -04:00
c1fb15b135 ci tweaks and aab fixes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m24s
2024-07-24 13:50:48 -04:00
4b2c131836 ci: Install doxygen for docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m30s
2024-07-24 13:04:52 -04:00
9ca1e69b3c Let's try to build in docker, too.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m48s
2024-07-24 12:56:27 -04:00
082d041d44 Update the android app icon / launch icon.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-07-24 12:50:31 -04:00
221f276c4b Simplify stats sending.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m16s
2024-07-24 12:15:05 -04:00
24cec21465 Move last remnant of static file handling from core to C. 2024-07-24 12:06:24 -04:00
9f71ec6194 Minor android cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m12s
2024-07-24 11:27:37 -04:00
bb36afc390 Use android ndk r27 (LTS) if available. 2024-07-24 11:20:35 -04:00
b53bf0ff64 Disallow rich text in the ssb compose box.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m13s
2024-07-22 14:42:37 -04:00
3ebc6f2436 Prettier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m14s
2024-07-22 14:19:12 -04:00
2eef6778a6 Latest CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m20s
2024-07-20 17:13:43 -04:00
81fabec810 Latest libbacktrace. 2024-07-20 17:12:43 -04:00
dc6e7924b5 Is this how I install dependencies on the gitea runner?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m14s
2024-07-18 12:42:08 -04:00
48dec5a2c8 More submodules.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 2m22s
2024-07-16 22:32:26 -04:00
9b500e1da9 Now actually build something.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7s
2024-07-16 22:30:36 -04:00
a038820112 Add the demo gitea action.
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 47s
2024-07-16 22:20:45 -04:00
70a15973b6 An fdroid build config that worked locally, for me. 2024-07-16 21:45:29 -04:00
09b6a00731 Fix android build with not enough -j. 2024-07-16 20:22:07 -04:00
883c3cf0e9 Clean up this core file. 2024-07-16 19:01:20 -04:00
a46bb8183c Fix OpenBSD compile. 2024-07-14 16:59:23 -04:00
d5d5a7b012 Build a separate .apk for fdroid with its own app ID. 2024-07-14 16:18:47 -04:00
a120efdc91 May as well dist the .aab. 2024-07-10 20:52:40 -04:00
d48f4b06eb Another f-droid directory. 2024-07-10 20:42:50 -04:00
f078912736 Add some recommended fdroid metadata. 2024-07-10 19:45:04 -04:00
63b0f0dedd Fix fdroid build with OpenSSL in-tree. 2024-07-10 19:35:49 -04:00
84c22dbf5f Move to OpenSSL as a git submodule. Redundant for platforms where it's not used, but makes fdroid easier. 2024-07-10 19:25:01 -04:00
b8cd1232be Have a little category, as a treat. 2024-07-09 19:30:33 -04:00
a518ab07f4 Add a WIP .fdroid.yml that seems to actually build something. 2024-07-09 19:28:45 -04:00
9e5a1ee975 Ugg. 2024-07-09 19:21:27 -04:00
95bf3f0316 This is almost doing something. 2024-07-09 19:19:01 -04:00
d69dd513bc Another silly fdroid test. 2024-07-09 19:08:40 -04:00
525cdf571a Testing a thing for fdroid. 2024-07-07 17:14:13 -04:00
9cfe0a8804 Add a 'JavaScript disabled' message. #56 2024-07-04 14:35:53 -04:00
50b54599ef Minor cleanup. 2024-07-04 13:18:23 -04:00
ed6bef6d24 Get android running its sandbox in a seprate, isolated service process. So that we support not extracting the native code from the APK, so that we support distributing as an .aab file, so that we may one day release on the app store. 2024-07-04 13:02:39 -04:00
71268636df Steps toward following all the inconvenient, changing android rules:
* Set android:debuggable=false.
 * Call native code through JNI only.  Having a native executable on disk and exec-ing it no longer seems possible.
 * Do all the Tilde Friends things in one process, without a proper sandbox, until I can wire up a restricted service worker process.
 * Jam Android App Bundle (.aab) building into the makefile.
 * Yuck.
2024-06-30 13:32:17 -04:00
568729ecd6 Stop auto-updating the version in default.nix. Will do it manually only on release. 2024-06-29 08:33:51 -04:00
9139725be6 Merge pull request 'build: fix the nix derivation' (#69) from tasiaiso/tildefriends:tasiaiso-0-0-20 into main
Reviewed-on: #69
2024-06-29 08:33:48 -04:00
969a8da6bf
build: update nix package to 0.0.20 2024-06-28 10:37:26 +02:00
2338b26329 Start working on 0.0.21. 2024-06-26 20:47:44 -04:00
d4df206740 Oh, I think I see how to nix this now. 2024-06-26 20:36:54 -04:00
8a93cdd33c Let's release 0.0.20. 2024-06-26 20:29:07 -04:00
92b31de4a9 Latest libbacktrace. 2024-06-26 20:20:41 -04:00
5452f3f623 Appease -fsanitize. 2024-06-26 20:20:34 -04:00
256614dbaf Actually stop stomping settings. 2024-06-26 19:58:59 -04:00
049449b213 I think this is how I lost settings. 2024-06-26 19:44:45 -04:00
85b46336b1 Better draft cleanup on submit. 2024-06-26 19:30:58 -04:00
590afa7b01 Fix content warnings. 2024-06-26 19:27:15 -04:00
574292b798 Reduce some common log noise. 2024-06-23 15:11:18 -04:00
21cf503a59 Fix a navigation bar option I neglected to button-ify. 2024-06-23 11:47:12 -04:00
3630cdbfe0 Consolidate the acount/login navigation bar options to try to save some space on mobile. 2024-06-20 20:41:27 -04:00
0f3be229e6 Actually, let's minify this thing using svgomg. 2024-06-20 20:07:58 -04:00
8e5a024d3d SVG favicon. 2024-06-20 20:05:00 -04:00
410bb7c09d Fix a ref count mistake and add a long-overdue tf_util_print_backtrace() that helped me find it. 2024-06-20 19:49:21 -04:00
9de8b0f449 Oops. 2024-06-20 12:36:21 -04:00
d47c3a1222 Fix a ref/unref mismatch. 2024-06-17 21:45:51 -04:00
df99b3aa90 Trying to catch an issue I think I saw in the debugger. 2024-06-17 21:23:48 -04:00
0090850e10 Forgot the other end of blobs.get. 2024-06-17 20:59:25 -04:00
9efd64bd18 Actually enforce _tf_ssb_assert_not_main_thread. 2024-06-17 12:36:54 -04:00
b16c37e48b Make ssb.privateMessageDecrypt do its work not on the main thread. I think that's finally everything for real. 2024-06-16 17:22:26 -04:00
3ee2c00726 Build fix. 2024-06-16 17:08:10 -04:00
d5a7e19f1a Move the bulk of ssb.privateMessageEncrypt work (CPU + DB) off the main thread. 2024-06-16 17:07:12 -04:00
9b52415b35 Make ssb.setServerFollowingMe not use the DB from the main thread. Two left?? 2024-06-16 16:22:59 -04:00
dbe24494d9 Remove ssb.messageContentGet. It's easy to do this with ssb.sqlAsync, and this wasn't being used productively. Three uses of DB on the main thread remaining. 2024-06-16 16:02:39 -04:00
3eab5a5f70 Make ssb.forgetStoredConnection not use the DB on the main thread. Four remaining? 2024-06-16 15:57:19 -04:00
548febfb22 Make ssb.storedConnections do its DB work not on the main thread. Five remaining by my new count? 2024-06-16 15:29:59 -04:00
b40f72443a A little format, as a treat. 2024-06-16 12:18:19 -04:00
2c03496373 Make databases.list, database.remove, and database.getLike all do their DB work off the main thread. That's the last thing I'm aware of. 2024-06-16 12:17:51 -04:00
b6a937c954 Move db.exchange DB work off of the main thread. 2024-06-16 10:16:39 -04:00
63776d40bd Update codemirror. 2024-06-16 09:23:14 -04:00
cb3c7afade Move ssb.getPrivateKey's DB work off the main thread. 2024-06-16 08:07:02 -04:00
991022adfc Move ssb.appendMessageWithIdentity's DB work off the main thread. 2024-06-16 07:51:06 -04:00
2bc71a18a6 Make ssb.deleteIdentity not block the main thread with DB work. 2024-06-14 17:39:24 -04:00
57ca864fbb Build fix. 2024-06-12 21:08:41 -04:00
a09edfb612 ssb.addIdentity without hitting the DB from the main thread. 2024-06-12 21:06:30 -04:00
7997a739ab ssb.createIdentity without hitting the database from the main thread. 2024-06-12 20:47:48 -04:00
248b258413 Make database.getAll() not block the main thread on database access. 2024-06-12 20:29:39 -04:00
0423ed7fb4 Login without hitting the DB from the main thread. 2024-06-12 20:12:35 -04:00
c29378c2f8 Yes, curl, follow redirects. 2024-06-10 21:19:06 -04:00
163fbd85e7 Fix docs. 2024-06-10 20:23:11 -04:00
58bb86ebe1 Make http.auth_query async and get its DB work off the main thread. 2024-06-10 20:22:28 -04:00
c5140ee8e8 Move DB work for ssb.getIdentities() and ssb.getAllIdentities() off the main thread. 2024-06-10 17:18:29 -04:00
6270fd8118 We don't need to go to the DB to get our public key. 2024-06-10 16:56:21 -04:00
3fff706848 Get the code of conduct and JWT signing key without hitting the database from the main thread. 2024-06-10 16:37:12 -04:00
c259defab5 Move database.get and database.set off the main thread. 2024-06-10 15:30:14 -04:00
e5fee5c306 Buildfix. 2024-06-10 12:01:49 -04:00
9d35b4bdfb Resuming work to move all DB access off the main thread. 2024-06-10 11:45:20 -04:00
9497d7cf64 Fix some shutdown hangs/leaks. 2024-06-06 20:31:24 -04:00
c7d3e602cb Fix &-mentions while I'm at it. 2024-06-06 20:14:00 -04:00
0076eb4ed4 Fix autocomplete again/more. #65 2024-06-06 20:05:24 -04:00
6070bde413 Avoid a null dereference. 2024-06-06 19:57:36 -04:00
c7a6d426f0 Fix autocomplete on Chrome, because contenteditable and shadowRoots are tricksy, and this module for @mentions is aging. #65 2024-06-06 19:52:37 -04:00
f66cf0f802 Unused. 2024-06-06 19:11:48 -04:00
e4b6c81024 No need to show your identity in the navigation bar if you have a name. 2024-06-06 18:51:40 -04:00
44d784cd04 OpenSSL 3.3.1. 2024-06-05 12:51:26 -04:00
0394201113 Merge pull request 'buld(nix): Misc Nix-related improvements' (#68) from tasiaiso/tildefriends:tasiaiso-nix-misc into main
Reviewed-on: #68
2024-06-04 20:16:15 -04:00
e270c16516 lit 3.1.4. 2024-06-04 20:10:27 -04:00
4c10538632
buld(nix): Misc Nix-related improvements
- Nixpkgs 23.11 is deprecated, use 24.05 instead
- update flake.lock
- add glibc as a build dependency
- add doxygen and graphviz as development dependencies for `make format`
2024-06-04 15:22:18 +02:00
71329c5532 format+prettier 2024-06-03 12:36:34 -04:00
feb4bf9e87 Limit message sends in a continued attempt to fix intermittent runaway memory usage. #64 2024-06-02 12:38:12 -04:00
5d5567e94c Reworking the emoji picker to use w3-modal, in a step toward doing the same for the currently broken @autocomplete. 2024-05-30 12:40:21 -04:00
684e6fb9cb Merge pull request 'nix: update version to 0.0.19' (#66) from tasiaiso/tildefriends:tasiaiso-nix-update into main
Reviewed-on: #66
2024-05-30 12:12:45 -04:00
ee21fa6d03
nix: update version to 0.0.19 2024-05-30 11:34:57 +02:00
7a2974e54f Working on 0.0.20. 2024-05-29 20:17:33 -04:00
f4dfc1dd98 Let's release 0.0.19. 2024-05-29 19:50:59 -04:00
2eebfa9a7a Make the websocket disconnect message not pop up a modal dialog so that it's less annoying when it happens in the normal course of events. #60 2024-05-27 20:35:40 -04:00
10097ffeb8 Update codemirror. 2024-05-27 08:23:45 -04:00
cbe1f54a2a libsodium 1.0.20. 2024-05-27 08:21:48 -04:00
4d8f081a59 Update libbacktrace to latest. 2024-05-27 08:21:10 -04:00
29e79c9484 Fix collapsing images taking extra clicks. 2024-05-25 08:09:44 -04:00
ba35869b0a sqlite 3.46.0. 2024-05-25 07:46:15 -04:00
580688381e prettier 2024-05-22 20:52:10 -04:00
e63d69a440 Missing generated semicolon. Sigh. 2024-05-22 20:44:28 -04:00
be64fe04fb Auto-update all the versions. 2024-05-22 20:35:48 -04:00
801ab20723 Merge pull request 'Add Nix support' (#62) from tasiaiso/tildefriends:tasiaiso-nix into main
Reviewed-on: #62
2024-05-22 20:31:11 -04:00
d974a5e044 An experiment in controlling memory usage when syncing. uv_read_stop when we have too active message/blob writes to the database and uv_read_start when we're back under control. #64 2024-05-22 19:53:33 -04:00
1be94ae0be Removed ssb.addEventListener and ssb.removeEventListener from the public API. Can do the same thing with core.register. 2024-05-22 18:51:21 -04:00
b883e6a485 Fix username/id extending off the screen in the welcome line. 2024-05-22 12:33:18 -04:00
a0210379ae Avoid confusing log output when responding with a method not found error. 2024-05-20 12:39:21 -04:00
e56dc207d1 Fix some shutdown issues in connection tracker code. 2024-05-16 12:41:48 -04:00
523c9c9ad2 Move mime type shenanigans from JS => C. 2024-05-15 19:25:48 -04:00
74bb2151c1 Fix shutdown issues with in-flight SSB connection attempts. 2024-05-15 12:37:13 -04:00
f79d7b35a4 Disallow creating accounts as a guest. #52 2024-05-14 12:41:17 -04:00
3b36496dac
chore: a bit more doc 2024-05-12 21:17:38 +02:00
4ebd6c24a9
chore: missing period in description 2024-05-12 21:15:30 +02:00
05451d98b3
Merge branch 'tasiaiso-nix' of https://dev.tildefriends.net/tasiaiso/tildefriends into tasiaiso-nix 2024-05-12 21:13:43 +02:00
22a4bce3c8
docs(nix): add documentation in default.nix 2024-05-12 21:13:22 +02:00
76d499f00b Merge branch 'main' into tasiaiso-nix 2024-05-12 14:56:12 -04:00
f0772f9b99
build(nix): add Nix support 2024-05-12 20:34:03 +02:00
46e711f0a5 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-12 10:40:14 -04:00
abffac3f82 Show missing profile images more deliberately. 2024-05-12 10:40:06 -04:00
27b275548e Fix docs. 2024-05-12 08:37:14 -04:00
93ce253d1e prettier 2024-05-12 08:23:34 -04:00
a5af312b39 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-12 08:23:23 -04:00
4b5e8e8a43 Consolidate similar request tags in the connection list. #59 2024-05-12 08:21:47 -04:00
443dd4d168 Merge pull request 'chore: code formatting' (#58) from tasiaiso/tildefriends:tasiaiso-format into main
Reviewed-on: #58
2024-05-12 08:05:02 -04:00
907479df84 Merge branch 'main' into tasiaiso-format 2024-05-12 07:52:33 -04:00
9887a78e98 prettier 2024-05-12 07:48:34 -04:00
f669371349 Show tab names on large enough screens. Inspired by tasio's #61. 2024-05-12 06:58:01 -04:00
24c720c79a Merge branch 'main' into tasiaiso-format 2024-05-12 02:06:09 -04:00
4485234980
chore(style): tell prettier to ignore code block 2024-05-12 08:01:37 +02:00
b6871c0b1f
chore: code formatting 2024-05-11 23:44:09 +02:00
47838d5e48 More name info issues. 2024-05-11 10:53:21 -04:00
69fccd56d3 Add a little guidance about how to set your name. It's a common confusion. 2024-05-11 10:40:34 -04:00
ca00c4fb5d Fix multiple issues getting identity info. 2024-05-11 10:23:07 -04:00
427ca3f265 Indicate both the server account and your own accounts in the ssb connections tab. 2024-05-11 09:58:24 -04:00
c1a80e50e7 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-11 09:50:06 -04:00
52962f3a5e Remove the :auth key. We can sign JWTs with :admin, and it's one less magic key. 2024-05-11 09:50:00 -04:00
b3f095b61f Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-11 09:33:48 -04:00
a5004c8ba9 Indicate the local server identity. 2024-05-11 09:33:38 -04:00
7d9b1b508b Print a little colorful message when we've started about where to connect. Multiple people have pointed out that it's not obvious that it's working. 2024-05-11 09:18:30 -04:00
5e265dfc83 Make sure the first user can admin. 2024-05-11 09:03:56 -04:00
3a43d6f8ac Build fix. 2024-05-11 09:03:37 -04:00
11a6649847 Add back a verify command. Remove unused and not very useful ssb.getMessage(). Make field ordering shenanigans more explicit. 2024-05-11 08:48:50 -04:00
7caf4a0173 Fix numerous issues around setting the first registered used as an admin. 2024-05-10 22:21:59 -04:00
385524352c Refactor most uses of uv_queue_work to go through a helper that keeps track of thread business, traces, and is generally less code. 2024-05-08 21:00:37 -04:00
5ca5323782 Fix /speedscope/ => deps/speedscope/index.html. 2024-05-08 20:57:53 -04:00
ba6da856bb Let trace truncate names more if it means we can generate valid JSON. 2024-05-08 20:56:44 -04:00
c0e72246cc Trying to understand a lingering 'previous message doesn't exist.' And format. 2024-05-08 12:20:57 -04:00
c7ab5447ea Move / redirect handling to C 2024-05-05 15:24:15 -04:00
5fdd461159 Fix setting multiline admin settings. 2024-05-05 15:19:00 -04:00
421955f2a0 getIdentityInfo => C. 2024-05-05 13:48:22 -04:00
a28f6985ed getActiveIdentity => C. 2024-05-05 12:55:32 -04:00
8244dddab7 Latest libbacktrace. 2024-05-05 12:03:57 -04:00
a5ca436eaa Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-02 20:35:24 -04:00
d7fc1c2c88 Minor style/layout changes. 2024-05-02 20:35:00 -04:00
382627ef8d Latest codemirror. 2024-05-02 20:11:54 -04:00
17667b4cf8 make format 2024-05-02 20:10:56 -04:00
5231ec22e7 More trying to clean up lingering requests. 2024-05-02 19:59:54 -04:00
929ae1b709 After eyeballing lingering requests, clean up requests after the response to an async (non-streaming) request is done. 2024-05-02 19:37:38 -04:00
f01f7a5ab9 Show active RPC requests in the connections tab. Probably TMI, but I want greater introspection into what is going on, and this seemed like a positive step. 2024-05-02 19:02:23 -04:00
a2dce833f8 Fix another shutdown issue. 2024-05-02 12:30:22 -04:00
de6c7a4fd4 SSB app stylin'. 2024-05-01 12:34:36 -04:00
4edee0f7f6 Allow importing from a single app .json. 2024-04-30 21:43:14 -04:00
988a807fa4 Admin app styles. 2024-04-30 19:18:33 -04:00
5258e4253d Better identity app layout on mobile. 2024-04-29 12:24:35 -04:00
09ba86dec5 Show replies to gatherings. 2024-04-28 12:55:17 -04:00
78d8a1aa23 Set the core room app icon. 2024-04-28 12:33:19 -04:00
22def15209 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-04-28 12:25:31 -04:00
4cbda7a849 Improve file errors so that it doesn't look like everything has failed when we see there's no https cert available. 2024-04-28 12:25:12 -04:00
be85a620ef Fix app delete. 2024-04-28 12:11:13 -04:00
0b07b678b4 Theme the identity app a bit. 2024-04-28 11:57:35 -04:00
4733ce9287 Fix ssb draft discard. 2024-04-28 11:23:28 -04:00
48d6bf4c15 Hook up onJsAlert on android. 2024-04-28 11:04:29 -04:00
8c759bcbac Hide the reactions button if there aren't any. 2024-04-26 18:19:13 -04:00
b5ed7014f6 Fix attaching files (aka WebView file picking) on Android. 2024-04-26 18:10:22 -04:00
6cd9dea186 Merge v0.0.18 compose fixes. 2024-04-24 20:35:36 -04:00
c5ddf3ac99 Fix some ssb compose issues. 2024-04-24 20:19:14 -04:00
a9cb913a47 Working on 0.0.19. 2024-04-24 19:29:17 -04:00
116 changed files with 14309 additions and 6437 deletions

View File

@ -0,0 +1,35 @@
name: Build Tilde Friends
run-name: ${{ gitea.actor }} running 🚀
on: [push]
jobs:
Build-All:
runs-on: ubuntu-latest
container:
valid_volumes: ['/opt/keys']
volumes:
- /opt/keys:/opt/keys
steps:
- name: check out code
uses: actions/checkout@v4
with:
submodules: true
- run: ln -s /opt/keys .keys
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
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
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
- run: docker build .
- uses: actions/upload-artifact@v3
with:
path: out/TildeFriends-release.fdroid.apk
- uses: actions/upload-artifact@v3
with:
path: out/winrelease/tildefriends.exe

7
.gitignore vendored
View File

@ -1,10 +1,17 @@
build/
*.core
db.*
deps/ios_toolchain/
deps/openssl/
dist/
.keys
logs/
**/node_modules
out
repo/
result
*.swo
*.swp
tmp/
unsigned/
.zsign_cache/

7
.gitmodules vendored
View File

@ -19,3 +19,10 @@
[submodule "deps/picohttpparser"]
path = deps/picohttpparser
url = https://github.com/h2o/picohttpparser.git
[submodule "deps/openssl_src"]
path = deps/openssl_src
url = https://github.com/openssl/openssl.git
shallow = true
[submodule "deps/c-ares"]
path = deps/c-ares
url = https://github.com/c-ares/c-ares.git

View File

@ -2,6 +2,7 @@ node_modules
src
deps
.clang-format
flake.lock
# Minified files
**/*.min.css

659
Doxyfile

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,12 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 18
VERSION_NUMBER := 0.0.18
VERSION_NAME := Celebrating totality for upwards of 3m1.4s.
VERSION_CODE := 27
VERSION_NUMBER := 0.0.23-wip
VERSION_NAME := Me upon my pony on my boat.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450300.zip
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
PROJECT = tildefriends
BUILD_DIR ?= out
@ -16,9 +16,13 @@ UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
ANDROID_SDK ?= ~/Android/Sdk
BUNDLETOOL = out/bundletool.jar
HAVE_WIN := 0
export SOURCE_DATE_EPOCH=1
export TZ=UTC
ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
else ifeq ($(UNAME_S),Linux)
@ -31,7 +35,8 @@ BUILD_TYPES := debug release
CFLAGS += -Dstatic_assert=_Static_assert
LDFLAGS += \
-lbsd \
-lnetwork
-lnetwork \
-Wno-stringop-overflow
else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release
CFLAGS += \
@ -50,18 +55,22 @@ CFLAGS += \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unknown-warning-option \
-MMD \
-MP \
-ffunction-sections \
-fdata-sections \
-fno-exceptions \
-g
LDFLAGS += \
-Wno-attributes \
-flto=auto
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
ANDROID_ARMV7A_TARGETS := \
out/androiddebug-armv7a/tildefriends \
@ -90,7 +99,7 @@ BUILD_TYPES += \
androidrelease-x86 \
androiddebug-x86_64 \
androidrelease-x86_64
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk out/TildeFriends-release.fdroid.apk
endif
WINDOWS_TARGETS := \
@ -150,19 +159,24 @@ 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))
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
$(ANDROID_TARGETS): CFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-fPIC \
-fdebug-compilation-dir . \
-fomit-frame-pointer \
-fno-asynchronous-unwind-tables \
-funwind-tables
-funwind-tables \
-Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
$(RELEASE_TARGETS): CFLAGS += \
-DNDEBUG \
-flto
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
@ -205,7 +219,7 @@ $(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
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
@ -238,6 +252,8 @@ APP_SOURCES_ios := $(wildcard src/*.m)
APP_OBJS := $(call get_objs,APP_SOURCES)
$(APP_OBJS): CFLAGS += \
-Ideps/base64c/include \
-Ideps/c-ares/include \
-Ideps/c-ares_config \
-Ideps/crypt_blowfish \
-Ideps/libbacktrace \
-Ideps/libsodium \
@ -256,6 +272,105 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
-fanalyzer
endif
ARES_SOURCES := \
deps/c-ares/src/lib/ares_platform.c \
deps/c-ares/src/lib/record/ares_dns_mapping.c \
deps/c-ares/src/lib/record/ares_dns_parse.c \
deps/c-ares/src/lib/record/ares_dns_write.c \
deps/c-ares/src/lib/record/ares_dns_name.c \
deps/c-ares/src/lib/record/ares_dns_record.c \
deps/c-ares/src/lib/record/ares_dns_multistring.c \
deps/c-ares/src/lib/ares_destroy.c \
deps/c-ares/src/lib/ares_data.c \
deps/c-ares/src/lib/ares_sysconfig.c \
deps/c-ares/src/lib/ares_cancel.c \
deps/c-ares/src/lib/ares_metrics.c \
deps/c-ares/src/lib/ares_getnameinfo.c \
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
deps/c-ares/src/lib/legacy/ares_create_query.c \
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
deps/c-ares/src/lib/legacy/ares_expand_name.c \
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
deps/c-ares/src/lib/legacy/ares_expand_string.c \
deps/c-ares/src/lib/legacy/ares_fds.c \
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
deps/c-ares/src/lib/legacy/ares_getsock.c \
deps/c-ares/src/lib/windows_port.c \
deps/c-ares/src/lib/ares_qcache.c \
deps/c-ares/src/lib/ares_update_servers.c \
deps/c-ares/src/lib/ares_process.c \
deps/c-ares/src/lib/ares_getenv.c \
deps/c-ares/src/lib/ares_gethostbyname.c \
deps/c-ares/src/lib/ares_send.c \
deps/c-ares/src/lib/dsa/ares__slist.c \
deps/c-ares/src/lib/dsa/ares__htable.c \
deps/c-ares/src/lib/dsa/ares__llist.c \
deps/c-ares/src/lib/dsa/ares__htable_szvp.c \
deps/c-ares/src/lib/dsa/ares__htable_asvp.c \
deps/c-ares/src/lib/dsa/ares__htable_vpvp.c \
deps/c-ares/src/lib/dsa/ares__htable_strvp.c \
deps/c-ares/src/lib/dsa/ares__array.c \
deps/c-ares/src/lib/ares__socket.c \
deps/c-ares/src/lib/event/ares_event_poll.c \
deps/c-ares/src/lib/event/ares_event_thread.c \
deps/c-ares/src/lib/event/ares_event_select.c \
deps/c-ares/src/lib/event/ares_event_kqueue.c \
deps/c-ares/src/lib/event/ares_event_configchg.c \
deps/c-ares/src/lib/event/ares_event_epoll.c \
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
deps/c-ares/src/lib/event/ares_event_win32.c \
deps/c-ares/src/lib/ares_search.c \
deps/c-ares/src/lib/ares__parse_into_addrinfo.c \
deps/c-ares/src/lib/ares__hosts_file.c \
deps/c-ares/src/lib/ares_getaddrinfo.c \
deps/c-ares/src/lib/ares__addrinfo2hostent.c \
deps/c-ares/src/lib/ares_freeaddrinfo.c \
deps/c-ares/src/lib/ares_strerror.c \
deps/c-ares/src/lib/ares_version.c \
deps/c-ares/src/lib/ares_gethostbyaddr.c \
deps/c-ares/src/lib/ares__addrinfo_localhost.c \
deps/c-ares/src/lib/ares_free_hostent.c \
deps/c-ares/src/lib/ares__close_sockets.c \
deps/c-ares/src/lib/ares_free_string.c \
deps/c-ares/src/lib/ares_init.c \
deps/c-ares/src/lib/ares_options.c \
deps/c-ares/src/lib/str/ares_strcasecmp.c \
deps/c-ares/src/lib/str/ares__buf.c \
deps/c-ares/src/lib/str/ares_strsplit.c \
deps/c-ares/src/lib/str/ares_str.c \
deps/c-ares/src/lib/ares_sysconfig_mac.c \
deps/c-ares/src/lib/ares__sortaddrinfo.c \
deps/c-ares/src/lib/ares_sysconfig_files.c \
deps/c-ares/src/lib/util/ares__iface_ips.c \
deps/c-ares/src/lib/util/ares__timeval.c \
deps/c-ares/src/lib/util/ares_math.c \
deps/c-ares/src/lib/util/ares_rand.c \
deps/c-ares/src/lib/util/ares__threads.c \
deps/c-ares/src/lib/ares_query.c \
deps/c-ares/src/lib/ares_cookie.c \
deps/c-ares/src/lib/inet_net_pton.c \
deps/c-ares/src/lib/inet_ntop.c \
deps/c-ares/src/lib/ares_library_init.c \
deps/c-ares/src/lib/ares_android.c \
deps/c-ares/src/lib/ares_sysconfig_win.c \
deps/c-ares/src/lib/ares_timeout.c
ARES_OBJS := $(call get_objs,ARES_SOURCES)
$(ARES_OBJS): CFLAGS += \
-Ideps/c-ares/include \
-Ideps/c-ares/src/lib \
-Ideps/c-ares_config/ \
-D_GNU_SOURCE \
-Wno-unused-function \
-Wno-deprecated-declarations \
-Wno-unused-result
BLOWFISH_SOURCES := \
deps/crypt_blowfish/crypt_blowfish.c \
deps/crypt_blowfish/crypt_gensalt.c \
@ -387,6 +502,8 @@ $(UV_OBJS): CFLAGS += \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable
$(UV_OBJS): CFLAGS += -fno-lto
$(filter out/win%,$(UV_OBJS)): CFLAGS += -Wno-cast-function-type
ifeq ($(UNAME_S),Linux)
$(UV_OBJS): CFLAGS += \
-D_GNU_SOURCE
@ -462,6 +579,7 @@ ifneq ($(UNAME_S),OpenBSD)
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
-DHAVE_ALLOCA_H
endif
$(SODIUM_OBJS): CFLAGS := $(filter-out -flto,$(CFLAGS))
SQLITE_SOURCES := deps/sqlite/sqlite3.c
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
@ -621,6 +739,7 @@ all: $(BUILD_TYPES)
ALL_APP_OBJS := \
$(APP_OBJS) \
$(ARES_OBJS) \
$(BLOWFISH_OBJS) \
$(LIBBACKTRACE_OBJS) \
$(MINIUNZIP_OBJS) \
@ -685,20 +804,37 @@ out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@mkdir -p $(dir $@)
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
@echo [aapt2 link] res.apk
@mkdir -p out/apk/
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--manifest src/android/AndroidManifest.xml \
-o out/apk/res.apk \
--java out/gen/
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@echo [aapt2 link] res.fdroid.apk
@mkdir -p out/apk/
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--rename-manifest-package com.unprompted.tildefriends.fdroid \
--manifest src/android/AndroidManifest.xml \
-o out/apk/res.fdroid.apk \
--java out/gen_fdroid/
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
$(CLASS_FILES) &: $(JAVA_FILES)
@echo "[javac] $(CLASS_FILES)"
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
out/apk/classes.dex: $(CLASS_FILES)
@mkdir -p $(dir $@)
@echo "[d8] $@"
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
PACKAGE_DIRS := \
apps/ \
@ -707,25 +843,79 @@ PACKAGE_DIRS := \
deps/prettier/ \
deps/lit/
RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-release.fdroid.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-release.fdroid.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.fdroid.apk
$(BUNDLETOOL):
@echo [curl] $(BUNDLETOOL_URL) TO $@
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
@rm -rf out/aab/staging/
@mkdir -p out/aab/staging
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
-I $(ANDROID_PLATFORM)/android.jar \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--manifest src/android/AndroidManifest.xml \
-R out/res/layout_activity_main.xml.flat \
-R out/res/drawable_icon.xml.flat \
--auto-add-overlay
@unzip out/aab/temporary.apk -d out/aab/staging/
@mkdir -p out/aab/staging/root/deps
@mkdir -p out/aab/staging/classes
@mkdir -p out/aab/staging/dex
@mkdir -p out/aab/staging/manifest
@mv out/aab/staging/AndroidManifest.xml out/aab/staging/manifest/AndroidManifest.xml
@cp out/apk/classes.dex out/aab/staging/dex/
@rm -fv out/base.zip
@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
@cp -r apps/ out/aab/staging/root/
@rm -rf out/aab/staging/root/apps/welcome*
@cp -r core/ out/aab/staging/root/
@cp -r deps/prettier/ out/aab/staging/root/deps/
@cp -r deps/lit/ out/aab/staging/root/deps/
@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
aab: out/TildeFriends.aab
.PHONY: aab
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
@java -jar $(BUNDLETOOL) build-apks --bundle out/TildeFriends.aab --overwrite --output $@ --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android
aabgo: out/TildeFriends.apks $(BUNDLETOOL)
@java -jar $(BUNDLETOOL) install-apks --apks out/TildeFriends.apks
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
out/apk/TildeFriends-arm-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@ -735,16 +925,38 @@ out/apk/TildeFriends-arm-%.unsigned.apk:
out/apk/TildeFriends-x86-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@zip -u $@.zip -q -9 $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/apk/TildeFriends-%.fdroid.unsigned.apk:
@rm -rf out/apk-fdroid-$(BUILD_TYPE) out/apk-fdroid-$(BUILD_TYPE)-raw
@mkdir -p $(dir $@) out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/ out/apk-fdroid-$(BUILD_TYPE)/lib/x86/ out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@cp out/apk/res.fdroid.apk $@.zip
@cp out/apk/classes.dex out/apk-fdroid-$(BUILD_TYPE)/classes.dex
@touch -d @0 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
@chmod 755 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
@cd out/apk-fdroid-$(BUILD_TYPE) && zip -X -u ../../$@.zip -q classes.dex lib/*/libtildefriends.so && cd ../../
@mkdir out/apk-fdroid-$(BUILD_TYPE)-raw
@for i in $(RAW_FILES); do mkdir -p $$(dirname out/apk-fdroid-$(BUILD_TYPE)-raw/$$i) && cp $$i out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && touch -d @0 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && chmod 644 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i; done
@cd out/apk-fdroid-$(BUILD_TYPE)-raw && zip -X -u ../../$@.zip -q $(RAW_FILES) && cd ../../
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/%.apk: out/apk/%.unsigned.apk
@echo "[apksigner] $(notdir $@)"
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
@ -757,9 +969,14 @@ out/%.zopfli.apk: out/%.apk
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
.PHONY: release-apk
apkgo: out/TildeFriends-arm-debug.apk
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: apkgo
releaseapkgo: out/TildeFriends-arm-release.apk
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.MainActivity
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo
# iOS Support
@ -820,10 +1037,6 @@ apklog:
.PHONY: apklog
fetchdeps:
@echo "[fetch] libuv"
@test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz)
@test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
@echo -n $(LIBUV_URL) > out/deps/libuv.txt
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@ -837,7 +1050,7 @@ fetchdeps:
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@tools/ssl-android
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(HAVE_WIN),1)
@ -854,14 +1067,27 @@ $(IOS_DEPS):
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/Tilde_Friends-x86_64.AppImage: out/release/tildefriends out/data.zip
@mkdir -p out/AppDir/usr/bin
@mkdir -p out/AppDir/usr/share/applications
@mkdir -p out/AppDir/usr/share/icons/hicolor/scalable/apps
@echo "[Desktop Entry]\nName=Tilde Friends\nExec=tildefriends\nIcon=tildefriends\nType=Application\nCategories=Network" > out/AppDir/usr/share/applications/tildefriends.desktop
@cp src/ios/tildefriends.svg out/AppDir/usr/share/icons/hicolor/scalable/apps/
@cat out/release/tildefriends out/data.zip > out/AppDir/usr/bin/tildefriends
@chmod +x out/AppDir/usr/bin/tildefriends
@unset SOURCE_DATE_EPOCH; cd out; linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage; cd ..
appimage: out/Tilde_Friends-x86_64.AppImage
.PHONY: appimage
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe)
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
tarball:
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf out/tildefriends-$(VERSION_NUMBER)
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
@mkdir -p out/tildefriends-$(VERSION_NUMBER)
@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
@tar \
--exclude=apps/welcome* \
@ -878,9 +1104,15 @@ dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.s
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
-caf out/tildefriends-$(VERSION_NUMBER).tar.xz \
-C out/ \
tildefriends-$(VERSION_NUMBER)
.PHONY: tarball
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage
@mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
@ -889,6 +1121,12 @@ dist: release-apk iosrelease-ipa $(if $(HAVE_WIN), out/winrelease/tildefriends.s
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@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"
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
@echo "[cp] TildeFriends-$(VERSION_NUMBER).fdroid.apk"
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/Tilde_Friends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
.PHONY: dist
dist-test: dist
@ -909,3 +1147,6 @@ prettier:
docs:
@doxygen
.PHONY: docs
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
.PHONY: fdroid

View File

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🎛"
"emoji": "🎛",
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
}

View File

@ -4,9 +4,38 @@
<script>
const g_data = $data;
</script>
<link rel="stylesheet" href="w3.css" />
<!-- prettier-ignore -->
<style>
/* 2018 Valiant Poppy */
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
.w3-text-theme {color:#bd3d3a !important}
.w3-border-theme {border-color:#bd3d3a !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
.w3-hover-text-theme:hover {color:#bd3d3a !important}
.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
</style>
</head>
<body style="color: #fff; width: 100%">
<h1>Tilde Friends Administration</h1>
<body class="w3-theme-l4">
<header class="w3-row w3-padding w3-header w3-theme-l1">
<h1>Tilde Friends Administration</h1>
</header>
</body>
<script type="module" src="script.js"></script>
</html>

View File

@ -27,64 +27,87 @@ function global_settings_set(key, value) {
});
}
function title_case(name) {
return name
.split('_')
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
.join(' ');
}
window.addEventListener('load', function () {
const permission_template = (permission) => html` <code>${permission}</code>`;
function input_template(key, description) {
if (description.type === 'boolean') {
return html`
<div style="margin-top: 1em">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div>
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
</li>
`;
} else if (description.type === 'textarea') {
return html`
<div style="margin-top: 1em"">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div style="width: 100%; padding: 0; margin: 0">
<div style="width: 90%; padding: 0 margin: 0">
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
</div>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
>${title_case(key)}</label
>
<div class="w3-rest w3-padding">${description.description}</div>
<textarea
class="w3-input"
style="vertical-align: top; resize: vertical"
id=${'gs_' + key}
>
${description.value}</textarea
>
<button
class="w3-button w3-right w3-quarter w3-theme-action"
@click=${(e) =>
global_settings_set(
key,
e.srcElement.previousElementSibling.value
)}
>
Set
</button>
</li>
`;
} else {
return html`
<div style="margin-top: 1em">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div>
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
</li>
`;
}
}
const user_template = (user, permissions) => html`
<li>
<button @click=${(e) => delete_user(user)}>Delete</button>
<li class="w3-card w3-margin">
<button
class="w3-button w3-theme-action"
@click=${(e) => delete_user(user)}
>
Delete
</button>
${user}: ${permissions.map((x) => permission_template(x))}
</li>
`;
const users_template = (users) =>
html`<h2>Users</h2>
<ul>
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
<ul class="w3-ul">
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<h2>Global Settings</h2>
<div>
${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
<div class="w3-container">
<ul class="w3-ul">
${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div>
${users_template(data.users)}
</div> `;

235
apps/admin/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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
"previous": "&zxsmzdLKsiG/WZt/Gw7JOxepgypoktNNbIyWiyFiJVc=.sha256"
}

View File

@ -18,8 +18,38 @@ tfrpc.register(async function reload() {
async function main() {
let ids = await ssb.getIdentities();
let server_id = await ssb.getServerIdentity();
await app.setDocument(
`<body style="color: #fff">
`
<head>
<link rel="stylesheet" href="w3.css"></link>
<style>
/* "2018 Sargasso Sea" */
.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important}
.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important}
.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important}
.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important}
.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important}
.w3-theme-d1 {color:#fff !important; background-color:#40485c !important}
.w3-theme-d2 {color:#fff !important; background-color:#394052 !important}
.w3-theme-d3 {color:#fff !important; background-color:#323848 !important}
.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important}
.w3-theme-d5 {color:#fff !important; background-color:#242833 !important}
.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important}
.w3-theme-dark {color:#fff !important; background-color:#242833 !important}
.w3-theme-action {color:#fff !important; background-color:#242833 !important}
.w3-theme {color:#fff !important; background-color:#485167 !important}
.w3-text-theme {color:#485167 !important}
.w3-border-theme {border-color:#485167 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important}
.w3-hover-text-theme:hover {color:#485167 !important}
.w3-hover-border-theme:hover {border-color:#485167 !important}
</style>
</head>
<body class="w3-theme-l3">
<script>const handler = {};</script>
<script type="module">
import * as tfrpc from '/static/tfrpc.js';
@ -27,7 +57,8 @@ async function main() {
let id = event.srcElement.dataset.id;
let element = document.createElement('textarea');
element.value = await tfrpc.rpc.get_private_key(id);
element.style = 'width: 100%; read-only: true';
element.style = 'width: 100%; height: auto; read-only: true; resize: none';
element.classList.add('w3-input');
element.readOnly = true;
event.srcElement.parentElement.appendChild(element);
event.srcElement.onclick = event => handler.hide_id(event, element);
@ -48,7 +79,7 @@ async function main() {
alert('Successfully created: ' + id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error creating identity: ' + e);
alert('Error creating identity: ' + e.message);
}
}
handler.hide_id = function hide_id(event, element) {
@ -69,23 +100,36 @@ async function main() {
}
}
</script>
<h1>SSB Identity Management</h1>
<h2>Create a new identity</h2>
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
<h2>Identities</h2>
<ul>` +
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
<footer class="w3-padding">
<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
</footer>
</div>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header>
<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea>
<footer class="w3-padding">
<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button>
</footer>
</div>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
<ul class="w3-ul">` +
ids
.map(
(id) => `<li>
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
${id}
</li>`
(
id
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
</li>`
)
.join('\n') +
` </ul>
</div>
</body>`
);
}

235
apps/identity/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

@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
ssb.addEventListener('message', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
@ -91,10 +88,12 @@ tfrpc.register(function getActiveIdentity() {
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
ssb.addEventListener('broadcasts', async function () {
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function () {
await tfrpc.rpc.set('connections', await ssb.connections());
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -55,7 +55,7 @@ function new_message() {
return g_new_message_promise;
}
ssb.addEventListener('message', function (id) {
core.register('onMessage', function (id) {
let resolve = g_new_message_resolve;
g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📦",
"previous": "&IU+TwyM7TznD8NBfnw7tgW2zxVlMqTVxSqWFjuosLwo=.sha256"
"emoji": "🚪",
"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&UDqtNEELPRZAP6jSrcKfoXpAr8s7GjWmWLOQINN4kmg=.sha256"
"previous": "&6oHPQCA26v+4nBXv+YUdCT43j2DpXDspxhHSSRydkiw=.sha256"
}

View File

@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
ssb.addEventListener('message', async function (id) {
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
@ -103,7 +103,7 @@ tfrpc.register(async function encrypt(id, recipients, content) {
tfrpc.register(async function getActiveIdentity() {
return await ssb.getActiveIdentity();
});
ssb.addEventListener('broadcasts', async function () {
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});

View File

@ -1,4 +1,6 @@
import * as tfrpc from '/static/tfrpc.js';
import {html, render} from './lit-all.min.js';
import {styles} from './tf-styles.js';
let g_emojis;
@ -36,11 +38,6 @@ export async function picker(callback, anchor, author) {
div.style.background = '#fff';
div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.position = 'absolute';
div.style.minWidth = 'min(16em, 90vw)';
div.style.width = 'min(16em, 90vw)';
div.style.maxWidth = 'min(16em, 90vw)';
div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
@ -58,14 +55,6 @@ export async function picker(callback, anchor, author) {
event.stopPropagation();
});
function cleanup() {
console.log('emoji cleanup');
div.parentElement.removeChild(div);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
function key_down(event) {
if (event.key == 'Escape') {
cleanup();
@ -153,13 +142,23 @@ export async function picker(callback, anchor, author) {
}
refresh();
input.oninput = refresh;
document.body.appendChild(div);
div.style.position = 'fixed';
div.style.top = '50%';
div.style.left = '50%';
div.style.transform = 'translate(-50%, -50%)';
let modal = html`
<style>
${styles}
</style>
<div class="w3-modal" style="display: block">
<div class="w3-modal-content w3-card-4">${div}</div>
</div>
`;
let parent = document.createElement('div');
document.body.appendChild(parent);
function cleanup() {
parent.parentElement.removeChild(parent);
window.removeEventListener('keydown', key_down);
document.body.removeEventListener('mousedown', cleanup);
}
render(modal, parent);
input.focus();
console.log('adding click');
document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -264,6 +264,7 @@ class TfElement extends LitElement {
hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
?loading=${this.loading}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@ -344,13 +345,15 @@ class TfElement extends LitElement {
([k, v]) => html`
<button
title=${v}
class="w3-bar-item w3-padding-large w3-hover-theme tab ${self.tab ==
v
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
? 'w3-theme-l2'
: 'w3-theme-l1'}"
@click=${() => self.set_tab(v)}
>
${k}
<span class=${self.tab == v ? '' : 'w3-hide-small'}
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
>
</button>
`
)}
@ -358,7 +361,12 @@ class TfElement extends LitElement {
`;
let contents = !this.loaded
? this.loading
? html`<div>Loading...</div>`
? html`<div
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
>
Loading...
</div>
${this.render_tab()}`
: html`<div>Select or create an identity.</div>`
: this.render_tab();
return html`
@ -366,8 +374,8 @@ class TfElement extends LitElement {
style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark"
>
${tabs}
<div style="padding: 8px">
${tabs}
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}

View File

@ -76,15 +76,9 @@ class TfComposeElement extends LitElement {
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.innerText);
let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById(
'content_warning_preview'
);
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
let draft = this.get_draft();
draft.text = edit.innerText;
draft.content_warning = content_warning?.innerText;
draft.content_warning = content_warning?.value;
setTimeout(() => this.notify(draft), 0);
}
@ -221,29 +215,19 @@ class TfComposeElement extends LitElement {
console.log('encrypted as', message);
}
try {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
edit.innerText = '';
self.input();
self.notify(undefined);
self.requestUpdate();
});
await tfrpc.rpc.appendMessage(this.whoami, message);
self.notify(undefined);
} catch (error) {
alert(error.message);
}
}
discard() {
let edit = this.renderRoot.getElementById('edit');
edit.innerText = '';
this.input();
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = '';
this.notify(undefined);
}
attach() {
let self = this;
let edit = this.renderRoot.getElementById('edit');
let input = document.createElement('input');
input.type = 'file';
input.onchange = function (event) {
@ -259,9 +243,9 @@ class TfComposeElement extends LitElement {
try {
let rows = await tfrpc.rpc.query(
`
SELECT json(messages.content) FROM messages_fts(?)
SELECT json(messages.content) AS content FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ?
WHERE json(messages.content) LIKE ?
ORDER BY timestamp DESC LIMIT 10
`,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
@ -297,18 +281,23 @@ class TfComposeElement extends LitElement {
);
}
let tribute = new Tribute({
iframe: this.shadowRoot,
collection: [
{
values: values,
selectTemplate: function (item) {
return item ? `[@${item.original.key}](${item.original.value})` : undefined;
return item
? `[@${item.original.key}](${item.original.value})`
: undefined;
},
},
{
trigger: '&',
values: this.autocomplete,
selectTemplate: function (item) {
return item ? `![${item.original.key}](${item.original.value})` : undefined;
return item
? `![${item.original.key}](${item.original.value})`
: undefined;
},
},
],
@ -327,6 +316,7 @@ class TfComposeElement extends LitElement {
let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) {
let tribute = new Tribute({
iframe: this.shadowRoot,
values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
@ -459,7 +449,7 @@ class TfComposeElement extends LitElement {
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<label for="cw">CW</label>
</p>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
</div>
`;
} else {
@ -548,9 +538,9 @@ class TfComposeElement extends LitElement {
id="edit"
@input=${this.input}
@paste=${this.paste}
contenteditable
contenteditable="plaintext-only"
.innerText=${live(draft.text ?? '')}
></span>
></span>
</div>
<div class="w3-half w3-padding">
${content_warning}

View File

@ -72,19 +72,21 @@ class TfMessageElement extends LitElement {
return expression;
}
}
return html`<div class="w3-button" @click=${this.show_reactions}>
${(this.message.votes || []).map(
(vote) => html`
<span
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
vote.timestamp
)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>`;
if (this.message?.votes?.length) {
return html`<div class="w3-button" @click=${this.show_reactions}>
${(this.message.votes || []).map(
(vote) => html`
<span
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
vote.timestamp
)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>`;
}
}
render_raw() {
@ -170,7 +172,7 @@ class TfMessageElement extends LitElement {
event.srcElement.classList.contains('img_caption')
) {
let next = event.srcElement.nextSibling;
if (next.style.display == 'block') {
if (next.style.display != 'none') {
next.style.display = 'none';
} else {
next.style.display = 'block';
@ -245,9 +247,7 @@ ${JSON.stringify(mention, null, 2)}</pre
if (mentions.length) {
let self = this;
return html`
<fieldset
style="backdrop-filter: brightness(1.2); padding: 0.5em; border: 1px solid black"
>
<fieldset style="padding: 0.5em; border: 1px solid black">
<legend>Mentions</legend>
${mentions.map((x) => self.render_mention(x))}
</fieldset>
@ -337,6 +337,9 @@ ${JSON.stringify(mention, null, 2)}</pre
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red'
: 'w3-theme-d4';
let self = this;
let raw_button;
switch (this.format) {
@ -395,8 +398,8 @@ ${JSON.stringify(mention, null, 2)}</pre
let body;
return html`
<div
class="w3-card-4"
style="backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
>
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"
@ -406,13 +409,24 @@ ${JSON.stringify(mention, null, 2)}</pre
>
${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}
></tf-message>
`
)}
</div>
`;
}
if (this.message?.type === 'contact_group') {
return html` <div
class="w3-card-4"
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) =>
@ -427,8 +441,8 @@ ${JSON.stringify(mention, null, 2)}</pre
</div>`;
} else if (this.message.placeholder) {
return html` <div
class="w3-card-4"
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
(placeholder)
@ -557,9 +571,6 @@ ${JSON.stringify(content, null, 2)}</pre
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'background-color: rgba(255, 0, 0, 0.2)'
: 'backdrop-filter: brightness(1.2)';
return html`
<style>
code {
@ -576,8 +587,8 @@ ${JSON.stringify(content, null, 2)}</pre
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; ${style_background}; margin-top: 8px; padding: 16px"
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
@ -603,9 +614,6 @@ ${JSON.stringify(content, null, 2)}</pre
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'background: rgba(255, 0, 0, 0.2)'
: 'backdrop-filter: brightness(1.2)';
return html`
<style>
code {
@ -622,8 +630,8 @@ ${JSON.stringify(content, null, 2)}</pre
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; ${style_background}; margin-top: 8px; padding: 16px"
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
@ -713,8 +721,8 @@ ${JSON.stringify(content, null, 2)}</pre
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; backdrop-filter: brightness(1.2); margin-top: 8px; padding: 16px"
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>

View File

@ -188,6 +188,10 @@ class TfProfileElement extends LitElement {
}
}
copy_id() {
navigator.clipboard.writeText(this.id);
}
render() {
if (
this.id == this.whoami &&
@ -287,6 +291,14 @@ class TfProfileElement extends LitElement {
let description = this.editing?.description ?? profile.description;
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<div class="w3-row">
<div class="w3-col s1 w3-container w3-right">
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
</div>
<div class="w3-rest w3-container">
<input type="text" class="w3-theme-d1" style="width: 100%; vertical-align: middle" readonly value=${this.id}></input>
</div>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">

View File

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

View File

@ -7,28 +7,43 @@ class TfTabConnectionsElement extends LitElement {
return {
broadcasts: {type: Array},
identities: {type: Array},
my_identities: {type: Array},
connections: {type: Array},
stored_connections: {type: Array},
users: {type: Object},
server_identity: {type: String},
};
}
static styles = styles;
static k_broadcast_emojis = {
discovery: '🏓',
room: '🚪',
peer_exchange: '🕸',
};
constructor() {
super();
let self = this;
this.broadcasts = [];
this.identities = [];
this.my_identities = [];
this.connections = [];
this.stored_connections = [];
this.users = {};
tfrpc.rpc.getIdentities().then(function (identities) {
self.my_identities = identities || [];
});
tfrpc.rpc.getAllIdentities().then(function (identities) {
self.identities = identities || [];
});
tfrpc.rpc.getStoredConnections().then(function (connections) {
self.stored_connections = connections || [];
});
tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity;
});
}
render_connection_summary(connection) {
@ -83,6 +98,7 @@ class TfTabConnectionsElement extends LitElement {
Connect
</button>
<div class="w3-bar-item">
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</div>
@ -96,6 +112,16 @@ class TfTabConnectionsElement extends LitElement {
}
render_connection(connection) {
let requests = Object.values(
connection.requests.reduce(function (accumulator, value) {
let key = `${value.name}:${Math.sign(value.request_number)}`;
if (!accumulator[key]) {
accumulator[key] = Object.assign({count: 0}, value);
}
accumulator[key].count++;
return accumulator;
}, {})
);
return html`
<button
class="w3-button w3-theme-d1"
@ -107,6 +133,20 @@ class TfTabConnectionsElement extends LitElement {
${connection.tunnel !== undefined
? '🚇'
: html`(${connection.host}:${connection.port})`}
<div>
${requests.map(
(x) => html`
<span class="w3-tag w3-small"
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
<span
class="w3-badge w3-white"
style=${x.count > 1 ? undefined : 'display: none'}
>${x.count}</span
></span
>
`
)}
</div>
<ul>
${this.connections
.filter((x) => x.tunnel === this.connections.indexOf(connection))
@ -175,6 +215,16 @@ class TfTabConnectionsElement extends LitElement {
${this.identities.map(
(x) =>
html`<li class="w3-bar">
${x == this.server_identity
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
>🖥 local server</span
>`
: undefined}
${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
>😎 you</span
>`
: undefined}
<tf-user id=${x} .users=${this.users}></tf-user>
</li>`
)}

View File

@ -12,6 +12,7 @@ class TfTabNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
loading: {type: Boolean},
};
}
@ -84,7 +85,6 @@ class TfTabNewsElement extends LitElement {
} else {
delete this.drafts[id];
}
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
@ -114,6 +114,19 @@ class TfTabNewsElement extends LitElement {
.users=${this.users}
></tf-profile>`
: undefined;
let edit_profile;
if (
!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami
) {
edit_profile = html` <div
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
>
Follow your identity link above to edit your profile and set your
name.
</div>`;
}
return html`
<p class="w3-bar">
<button
@ -123,8 +136,9 @@ class TfTabNewsElement extends LitElement {
${this.new_messages_text()}
</button>
</p>
<div>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose

View File

@ -5,6 +5,7 @@ import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement {
static get properties() {
return {
drafts: {type: Object},
whoami: {type: String},
users: {type: Object},
following: {type: Array},
@ -22,6 +23,10 @@ class TfTabSearchElement extends LitElement {
this.users = {};
this.following = [];
this.expanded = {};
this.drafts = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
async search(query) {
@ -70,6 +75,18 @@ class TfTabSearchElement extends LitElement {
}
}
draft(event) {
let id = event.detail.id || '';
let previous = this.drafts[id];
if (event.detail.draft !== undefined) {
this.drafts[id] = event.detail.draft;
} else {
delete this.drafts[id];
}
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
@ -81,7 +98,7 @@ class TfTabSearchElement extends LitElement {
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
`;
}
}

View File

@ -19,6 +19,11 @@ class TfUserElement extends LitElement {
}
render() {
let image = html`<span
class="w3-theme-light w3-circle"
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
>?</span
>`;
let name = this.users?.[this.id]?.name;
name =
name !== undefined
@ -26,21 +31,20 @@ class TfUserElement extends LitElement {
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
if (this.users[this.id]) {
let image = this.users[this.id].image;
image = typeof image == 'string' ? image : image?.link;
return html` <div style="display: inline-block; font-weight: bold">
<img
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
?hidden=${image === undefined}
src="${image ? '/' + image + '/view' : undefined}"
/>
${name}
</div>`;
} else {
return html` <div style="display: inline-block; font-weight: bold">
${name}
</div>`;
let image_link = this.users[this.id].image;
image_link =
typeof image_link == 'string' ? image_link : image_link?.link;
if (image_link !== undefined) {
image = html`<img
class="w3-circle"
style="width: 2em; height: 2em; vertical-align: middle"
src="/${image_link}/view"
/>`;
}
}
return html` <div style="display: inline-block; font-weight: bold">
${image} ${name}
</div>`;
}
}

View File

@ -1,5 +1,7 @@
import * as hashtagify from './commonmark-hashtag.js';
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
@ -60,10 +62,20 @@ function image(node, entering) {
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
@ -72,6 +84,7 @@ export function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
parsed = hashtagify.transform(parsed);

View File

@ -482,16 +482,7 @@ class TributeRange {
}
getDocument() {
let iframe;
if (this.tribute.current.collection) {
iframe = this.tribute.current.collection.iframe;
}
if (!iframe) {
return document
}
return iframe.contentWindow.document
return document;
}
positionMenuAtCaret(scrollTo) {
@ -653,8 +644,8 @@ class TributeRange {
}
getWindowSelection() {
if (this.tribute.collection.iframe) {
return this.tribute.collection.iframe.contentWindow.getSelection()
if (this.tribute.collection[0].iframe?.getSelection) {
return this.tribute.collection[0].iframe.getSelection()
}
return window.getSelection()

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&W5aJp2DgOW5rQ0AOIC9Ut3DpsahPrO6PjkJ1PQbNRdM=.sha256"
"previous": "&7Pqk5nBAcbjzp0etv6WgiyTD3UF++ID0mW6qIbhwt3s=.sha256"
}

124
apps/welcome/f-droid.svg Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Original Author: Unknown (if you are the original creator of the F-Droid button, please contact laura@ind.ie so I can credit you!) -->
<!-- Author: Created by Laura Kalbag and Released with ❤ by ind.ie (laura@ind.ie) -->
<!-- License: This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/. (As attribution is included in this file, you needn't include additional attribution on your site.) -->
<!-- How to use: The original blog post about this SVG button and how I use SVG on the Ind.ie website is at https://ind.ie/blog/f-droid-button/ -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="button_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="1490 188 300 104" enable-background="new 1490 188 300 104" xml:space="preserve">
<g id="get_it_on_f-droid_2_">
<path id="button" d="M1780,292h-280c-5.5,0-10-4.5-10-10v-84c0-5.5,4.5-10,10-10h280c5.5,0,10,4.5,10,10v84
C1790,287.5,1785.5,292,1780,292z"/>
<g id="f-droid_2_">
<path fill="#FFFFFF" d="M1621.2,236.8v3.6v1.2v5h-0.3h-0.6h-2.2c-0.7,0-1-0.3-1.2-1c-0.1-0.4-0.2-1.6-0.3-3.4h-13.5v11.1h13.2v5.5
h-13.2v10.1c0.9,0.2,1.7,0.3,2.6,0.4c0.9,0.2,1.3,0.7,1.3,1.6v3h-3.9h-7.1h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6c0.9-0.2,1.7-0.3,2.6-0.4
V242c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h22.7h1.2L1621.2,236.8L1621.2,236.8z"/>
<path fill="#FFFFFF" d="M1637.2,256v5.4h-13.5V256H1637.2z"/>
<path fill="#FFFFFF" d="M1676.7,255.6c0,5-1.7,10-5.3,13.5c-3.7,3.6-8.8,5.3-13.9,5.3h-13.9h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6
c0.9-0.2,1.7-0.3,2.6-0.4V242c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h13.9c5.1,0,10.2,1.6,13.9,5.3
C1675.1,245.6,1676.7,250.7,1676.7,255.6C1676.7,258.4,1676.7,252.9,1676.7,255.6z M1669.6,255.6c0-3.4-0.8-6.9-3.1-9.5
s-5.6-3.7-8.9-3.7h-6.8v26.4h6.8c3.4,0,6.7-1.1,8.9-3.7C1668.8,262.5,1669.6,259,1669.6,255.6z"/>
<path fill="#FFFFFF" d="M1699.7,248.1l-0.9,4.8c-0.2,1.1-1.3,0.9-2.1,0.7c-1-0.3-2.2-0.3-3.1,0c-2.2,0.5-3.7,2.3-4.5,4.2v11.4
c2.3,0.4,2.3,0.4,2.6,0.4c0.9,0.2,1.3,0.7,1.3,1.6v3h-3.9l0,0h-6.5h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6c0.4-0.1,0.4-0.1,2.6-0.4v-16.3
c-2.3-0.4-2.3-0.4-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9l0,0h3.8c1.1,0,1.7,0.4,1.9,1.6l0.3,3.2c1.1-1.9,2.6-3.7,4.7-4.7
C1695.2,247,1697.8,246.9,1699.7,248.1z"/>
<path fill="#FFFFFF" d="M1719.4,248.3c5.7,2.3,8,8,7.8,13.7c-0.3,5.6-3.4,10.7-9.1,12.3c-5.4,1.5-11.9,0-15.1-4.9
c-3.1-4.6-3.2-11.6-0.3-16.4C1706,247.6,1713.6,246,1719.4,248.3C1721,248.9,1717.8,247.6,1719.4,248.3z M1718.9,267.6
c2-2.8,2-7.1,1.2-10.2c-0.3-1.5-1-2.9-2.2-3.9c-1.3-1-3-1.4-4.6-1.2c-3.5,0.2-5.3,2.7-5.8,5.9c-0.5,3.1-0.5,7.5,1.8,10
C1711.8,270.7,1716.8,270.5,1718.9,267.6C1720,266.2,1717.9,269.1,1718.9,267.6z"/>
<path fill="#FFFFFF" d="M1743.3,271.4v3h-3.9h-6.5h-4v-3c0-0.9,0.4-1.5,1.3-1.6c0.9-0.2,1.7-0.3,2.6-0.4v-16.4
c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h6.5v21.5c0.9,0.2,1.7,0.3,2.6,0.4
C1742.9,269.9,1743.3,270.5,1743.3,271.4z M1740,241.6c-1,2-3.4,3-5.4,2.2c-2-0.9-3.1-3.3-2.2-5.3c0.9-2.1,3.3-3,5.4-2.2
C1739.9,237.1,1741,239.5,1740,241.6C1739.8,242,1740.3,241,1740,241.6z"/>
<path fill="#FFFFFF" d="M1773.4,274.3h-3.7h-3.9c-0.8,0-1.5-0.3-1.7-1.2l-0.5-2.4c-2.4,2.8-5.9,4.5-9.8,4.1
c-3.8-0.4-6.5-3.2-7.8-6.6c-2.4-6.6-1.3-16.3,5.6-19.8c3.8-1.9,8.5-1.5,11.6,1.5V241c-0.9-0.2-1.7-0.3-2.6-0.4
c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h6.5v33.5c0.8,0.2,1.6,0.3,2.4,0.4c0.9,0.2,1.3,0.7,1.3,1.6V274.3z M1763.3,254.6
c-1.7-2-4.2-2.9-6.7-2.3c-2.5,0.6-3.9,2.8-4.4,5.1c-0.5,2.3-0.5,5-0.1,7.4c0.4,2.3,1.7,4.4,4.1,4.9c2.9,0.5,5.4-1,7.2-3.1V254.6z"
/>
</g>
<g id="get_it_on_2_">
<path fill="#FFFFFF" d="M1597.5,218.2v-2.9h7.6v6.9c-3.7,3.6-11.2,3.9-14.5-0.3c-3.3-4.2-2.4-11.5,2.6-14c2.2-1.1,5.1-1.1,7.3-0.4
s3.8,2.4,4.3,4.7l-3.5,0.7c-0.7-2.5-3.5-3.3-5.8-2.5c-2.2,0.7-3.1,2.8-3.2,4.9c-0.1,2.2,0.3,4.6,2.1,5.9c2.1,1.6,5.1,0.9,7-0.6
v-2.3H1597.5z"/>
<path fill="#FFFFFF" d="M1608.3,224.7v-17.3h13v2.9h-9.4v3.8h8.8v2.9h-8.8v4.8h9.8v2.9L1608.3,224.7L1608.3,224.7z"/>
<path fill="#FFFFFF" d="M1628.6,224.7v-14.4h-5.1v-2.9h13.9v2.9h-5.1v14.5L1628.6,224.7L1628.6,224.7z"/>
<path fill="#FFFFFF" d="M1646.3,224.7v-17.3h3.5v17.3H1646.3z"/>
<path fill="#FFFFFF" d="M1657.1,224.7v-14.4h-5.1v-2.9h13.9v2.9h-5.1v14.5L1657.1,224.7L1657.1,224.7z"/>
<path fill="#FFFFFF" d="M1674.1,216.1c0-1.7,0.3-3.3,0.8-4.4c0.8-1.7,2.2-3.2,3.9-4c2.8-1.1,6.5-1,9,0.9c2.5,1.8,3.4,4.9,3.3,7.8
c-0.1,2.9-1.1,5.8-3.8,7.5c-2.5,1.6-6.1,1.6-8.7,0.3C1675.4,222.7,1674.1,219.4,1674.1,216.1z M1677.8,216c0,2.3,0.7,4.8,3.1,5.6
c2.2,0.9,4.8,0,5.8-2c1-1.9,1-4.9,0.3-6.8c-0.9-2.1-3.1-3.1-5.3-2.7C1678.7,210.6,1677.8,213.4,1677.8,216z"/>
<path fill="#FFFFFF" d="M1693.9,224.7v-17.3h3.4l7.2,11.6v-11.6h3.3v17.3h-3.6l-7.1-11.4v11.4H1693.9z"/>
</g>
<g id="droid_2_">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="440.5436" x2="413.9844" y2="452.6552" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
<stop offset="0" style="stop-color:#2B6099"/>
<stop offset="0.1299" style="stop-color:#2F69A1"/>
<stop offset="0.3451" style="stop-color:#3B83B6"/>
<stop offset="0.5" style="stop-color:#4699C8"/>
<stop offset="0.9944" style="stop-color:#479ECB"/>
</linearGradient>
<path fill="url(#SVGID_1_)" d="M1570.1,275.2h-59.3c-2.9,0-5.2-2.3-5.2-5.2v-34.7c0-2.9,2.4-5.2,5.2-5.2h59.3
c2.9,0,5.2,2.3,5.2,5.2V270C1575.3,272.8,1572.9,275.2,1570.1,275.2z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="440.5436" x2="413.9844" y2="452.6552" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
<stop offset="0" style="stop-color:#2B6099"/>
<stop offset="0.5" style="stop-color:#58A4CD"/>
<stop offset="0.7865" style="stop-color:#7FB8D9"/>
<stop offset="1" style="stop-color:#9EC9E2"/>
</linearGradient>
<path fill="url(#SVGID_2_)" d="M1570.1,231.9c1.9,0,3.5,1.6,3.5,3.5V270c0,1.9-1.6,3.5-3.5,3.5h-59.3c-1.9,0-3.5-1.6-3.5-3.5
v-34.7c0-1.9,1.6-3.5,3.5-3.5H1570.1 M1570.1,230.1h-59.3c-2.9,0-5.2,2.3-5.2,5.2V270c0,2.9,2.4,5.2,5.2,5.2h59.3
c2.9,0,5.2-2.3,5.2-5.2v-34.7C1575.3,232.5,1572.9,230.1,1570.1,230.1L1570.1,230.1z"/>
</g>
<g>
<path fill="#295384" d="M1540.4,236.6c8.9,0,16.1,7.2,16.1,16c0,8.8-7.2,16-16.1,16s-16.1-7.2-16.1-16S1531.5,236.6,1540.4,236.6
M1540.4,235.3c-9.6,0-17.4,7.8-17.4,17.3s7.8,17.3,17.4,17.3s17.4-7.8,17.4-17.3S1550.1,235.3,1540.4,235.3L1540.4,235.3z"/>
</g>
<path fill="#295384" d="M1535.4,251.1c1.9-5.7,10.7-3.9,10.2,2.2c-0.5,5.6-8.5,6.2-10.2,1.2c0,0,0,0.1,0,0l0,0
c-2.4,0.2-4.7,0.4-7.1,0.6c1.7,10.1,15.3,13.1,21.6,5.3c6.5-8,0.3-20.2-10-19.8c-5.6,0.3-10.5,4.3-11.5,9.8"/>
<path fill="#7B952D" d="M1580.3,198.7l-2-1.6c-0.3-0.3-1-0.3-1.2,0.1l-6.8,7.9c-0.3,0.3-0.3,1,0.1,1.2l2,1.6
c0.3,0.3,1,0.3,1.2-0.1l6.8-7.9C1580.7,199.6,1580.6,199,1580.3,198.7z"/>
<path fill="#7B952D" d="M1500.6,198.1l2-1.6c0.3-0.3,1-0.3,1.2,0.1l6.8,7.9c0.3,0.3,0.3,1-0.1,1.2l-2,1.6c-0.3,0.3-1,0.3-1.2-0.1
l-6.8-7.9C1500.2,199,1500.3,198.5,1500.6,198.1z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="453.354" x2="413.9844" y2="459.4099" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
<stop offset="0" style="stop-color:#BCDB52"/>
<stop offset="0.3206" style="stop-color:#C5E358"/>
<stop offset="0.5" style="stop-color:#CDEA5C"/>
<stop offset="0.9944" style="stop-color:#DCF285"/>
</linearGradient>
<path fill="url(#SVGID_3_)" stroke="#7B952D" stroke-miterlimit="10" d="M1572.7,227.5h-64.5c-1,0-1.7-0.8-1.7-1.7v-19.1
c0-1,0.8-1.7,1.7-1.7h64.5c1,0,1.7,0.8,1.7,1.7v19.1C1574.4,226.7,1573.6,227.5,1572.7,227.5z"/>
<g>
<ellipse fill="#FFFFFF" cx="1523" cy="215.4" rx="6.1" ry="6.1"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="409.8439" y1="458.401" x2="408.7539" y2="454.8358" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
<stop offset="0" style="stop-color:#8BA53D"/>
<stop offset="8.484717e-02" style="stop-color:#94AE45"/>
<stop offset="0.2259" style="stop-color:#AEC65C"/>
<stop offset="0.4048" style="stop-color:#D7ED81"/>
<stop offset="0.4246" style="stop-color:#DCF285"/>
</linearGradient>
<path fill="url(#SVGID_4_)" d="M1523,222.3c-3.8,0-7-3.1-7-6.9c0-3.8,3.1-6.9,7-6.9c3.8,0,7,3.1,7,6.9
C1529.9,219.2,1526.9,222.3,1523,222.3z M1523,210.2c-2.9,0-5.2,2.3-5.2,5.2s2.4,5.2,5.2,5.2s5.2-2.3,5.2-5.2
S1525.9,210.2,1523,210.2z"/>
</g>
<g>
<ellipse fill="#FFFFFF" cx="1557.8" cy="215.4" rx="6.1" ry="6.1"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="419.2189" y1="458.401" x2="418.129" y2="454.8359" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
<stop offset="0" style="stop-color:#8BA53D"/>
<stop offset="8.484717e-02" style="stop-color:#94AE45"/>
<stop offset="0.2259" style="stop-color:#AEC65C"/>
<stop offset="0.4048" style="stop-color:#D7ED81"/>
<stop offset="0.4246" style="stop-color:#DCF285"/>
</linearGradient>
<path fill="url(#SVGID_5_)" d="M1557.8,222.3c-3.8,0-7-3.1-7-6.9c0-3.8,3.1-6.9,7-6.9c3.8,0,7,3.1,7,6.9
C1564.8,219.2,1561.8,222.3,1557.8,222.3z M1557.8,210.2c-2.9,0-5.2,2.3-5.2,5.2s2.4,5.2,5.2,5.2c2.9,0,5.2-2.3,5.2-5.2
S1560.8,210.2,1557.8,210.2z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -68,6 +68,9 @@
href="https://dev.tildefriends.net/"
><i class="fa fa-mug-hot"></i> Code</a
>
<p>
<a href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"><img src="f-droid.svg" style="height: 3em"></a>
</p>
</div>
<div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
@ -270,6 +273,13 @@
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
<p>Lit</p>
</a>
<a href="https://github.com/c-ares/c-ares" class="w3-col s3">
<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
<p>c-ares</p>
</a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
<p>GNU Make</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -50,7 +50,7 @@ function new_message() {
return g_new_message_promise;
}
ssb.addEventListener('message', function (id) {
core.register('onMessage', function (id) {
let resolve = g_new_message_resolve;
g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve;

View File

@ -83,10 +83,10 @@ App.prototype.send = function (message) {
* @param {*} response
* @param {*} client
*/
function socket(request, response, client) {
async function socket(request, response, client) {
let process;
let options = {};
let credentials = httpd.auth_query(request.headers);
let credentials = await httpd.auth_query(request.headers);
response.onClose = async function () {
if (process && process.task) {
@ -149,7 +149,7 @@ function socket(request, response, client) {
parentApp: parentApp,
id: blobId,
},
await core.getIdentityInfo(
await ssb.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName
@ -211,10 +211,6 @@ function socket(request, response, client) {
if (process && process.timeout > 0) {
setTimeout(ping, process.timeout);
}
} else if (message.action == 'enableStats') {
if (process) {
core.enableStats(process, message.enabled);
}
} else if (message.action == 'resetPermission') {
if (process) {
process.resetPermission(message.permission);
@ -222,7 +218,7 @@ function socket(request, response, client) {
} else if (message.action == 'setActiveIdentity') {
process.setActiveIdentity(message.identity);
} else if (message.action == 'createIdentity') {
process.createIdentity();
await process.createIdentity();
} else if (message.message == 'tfrpc') {
if (message.id && g_calls[message.id]) {
if (message.error !== undefined) {

View File

@ -3,7 +3,7 @@
<head>
<title>Tilde Friends Sign-in</title>
<link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>

View File

@ -10,6 +10,7 @@ let gEditor;
let gOriginalInput;
let kErrorColor = '#dc322f';
let kDisconnectColor = '#f00';
let kStatusColor = '#fff';
// Functions that server-side app code can call through the app object.
@ -117,28 +118,6 @@ class TfNavigationElement extends LitElement {
return this.spark_lines[key];
}
/**
* TODOC
* @returns
*/
render_login() {
if (this?.credentials?.session?.name) {
return html`<a
class="w3-bar-item w3-right"
id="login"
href="/login/logout?return=${url() + hash()}"
>logout ${this.credentials.session.name}</a
>`;
} else {
return html`<a
class="w3-bar-item w3-right"
id="login"
href="/login?return=${url() + hash()}"
>login</a
>`;
}
}
set_active_identity(id) {
send({action: 'setActiveIdentity', identity: id});
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
@ -158,67 +137,105 @@ class TfNavigationElement extends LitElement {
window.location.href = '/~core/ssb/#' + this.identity;
}
logout() {
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
}
render_identity() {
let self = this;
if (this.identities?.length) {
return html`
<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"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
@click=${self.toggle_id_dropdown}
>
${self.names[this.identity]}${self.names[this.identity] ===
this.identity
? ''
: html` - ${this.identity}`}
</button>
<div
id="id_dropdown"
class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%"
>
if (this?.credentials?.session?.name) {
if (this.identities?.length) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
<button
class="w3-bar-item w3-button w3-border"
@click=${() => (window.location.href = '/~core/identity')}
class="w3-button w3-rest w3-cyan"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
id="identity"
@click=${self.toggle_id_dropdown}
>
Manage Identities...
${self.names[this.identity]}
</button>
<button
class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile}
<div
id="id_dropdown"
class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%; right: 0"
>
Edit Profile...
</button>
${this.identities.map(
(x) => html`
<button
class="w3-bar-item w3-button ${x === self.identity
? 'w3-cyan'
: ''}"
@click=${() => self.set_active_identity(x)}
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
>
${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
</button>
`
)}
<button
class="w3-bar-item w3-button w3-border"
@click=${() => (window.location.href = '/~core/identity')}
>
Manage Identities...
</button>
<button
class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile}
>
Edit Profile...
</button>
${this.identities.map(
(x) => html`
<button
class="w3-bar-item w3-button ${x === self.identity
? 'w3-cyan'
: ''}"
@click=${() => self.set_active_identity(x)}
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
>
${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
</button>
`
)}
<button
class="w3-bar-item w3-button w3-border"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
</div>
</div>
</div>
`;
`;
} else if (
this.credentials?.session?.name &&
this.credentials.session.name !== 'guest'
) {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<button
class="w3-bar-item w3-button w3-right w3-cyan"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
<button
id="create_identity"
@click=${this.create_identity}
class="w3-button w3-mobile w3-red w3-right"
>
Create an Identity
</button>
`;
} else {
return html`
<button
class="w3-bar-item w3-button w3-right w3-cyan"
id="logout"
@click=${self.logout}
>
Logout ${this.credentials.session.name}
</button>
`;
}
} else {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<button
id="create_identity"
@click=${this.create_identity}
class="w3-button w3-mobile w3-blue w3-right"
>
Create an Identity
</button>
`;
return html`<a
class="w3-bar-item w3-cyan w3-right"
id="login"
href="/login?return=${url() + hash()}"
>login</a
>`;
}
}
@ -358,7 +375,7 @@ class TfNavigationElement extends LitElement {
${Object.keys(this.spark_lines)
.sort()
.map((x) => this.spark_lines[x])}
${this.render_login()} ${this.render_identity()}
${this.render_identity()}
</div>
${this.status?.is_error
? html`
@ -1257,7 +1274,6 @@ function _receive_websocket_message(message) {
document.getElementById('viewPane').style.display = message.edit_only
? 'none'
: 'flex';
send({action: 'enableStats', enabled: true});
} else if (message && message.action == 'ping') {
send({action: 'pong'});
} else if (message && message.action == 'stats') {
@ -1545,7 +1561,7 @@ function connectSocket(path) {
};
setStatusMessage(
'🔴 Closed: ' + (k_codes[event.code] || event.code),
kErrorColor
kDisconnectColor
);
};
}

View File

@ -8,120 +8,6 @@ let gStatsTimer = false;
const k_content_security_policy =
'sandbox allow-downloads allow-top-navigation-by-user-activation';
const k_mime_types = {
css: 'text/css',
html: 'text/html',
js: 'text/javascript',
json: 'text/json',
map: 'application/json',
svg: 'image/svg+xml',
};
const k_magic_bytes = [
{bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'},
{
bytes: [
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
],
type: 'image/jpeg',
},
{bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'},
{
bytes: [
0xff,
0xd8,
0xff,
0xe1,
null,
null,
0x45,
0x78,
0x69,
0x66,
0x00,
0x00,
],
type: 'image/jpeg',
},
{bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'},
{bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'},
{bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'},
{
bytes: [
0x52,
0x49,
0x46,
0x46,
null,
null,
null,
null,
0x57,
0x45,
0x42,
0x50,
],
type: 'image/webp',
},
{bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'},
{
bytes: [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x6d,
0x70,
0x34,
0x32,
],
type: 'audio/mpeg',
},
{
bytes: [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x69,
0x73,
0x6f,
0x6d,
],
type: 'video/mp4',
},
{
bytes: [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x6d,
0x70,
0x34,
0x32,
],
type: 'video/mp4',
},
{bytes: [0x4d, 0x54, 0x68, 0x64], type: 'audio/midi'},
];
let k_static_files = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
];
const k_global_settings = {
index: {
type: 'string',
@ -137,13 +23,18 @@ const k_global_settings = {
room: {
type: 'boolean',
default_value: true,
description: 'Whether this instance should behave as a room.',
description: 'Enable peers to tunnel through this instance as a room.',
},
room_name: {
type: 'string',
default_value: 'tilde friends tunnel',
description: 'Name of the room.',
},
replicator: {
type: 'boolean',
default_value: true,
description: 'Enable message and blob replication.',
},
code_of_conduct: {
type: 'textarea',
default_value: undefined,
@ -178,6 +69,22 @@ const k_global_settings = {
: undefined,
description: 'Blobs older than this will be automatically deleted.',
},
seeds_host: {
type: 'string',
default_value: 'seeds.tildefriends.net',
description: 'Hostname for seed connections.',
},
peer_exchange: {
type: 'boolean',
default_value: false,
description:
'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
},
account_registration: {
type: 'boolean',
default_value: true,
description: 'Allow registration of new accounts.',
},
};
let gGlobalSettings = {
@ -316,7 +223,7 @@ function getUser(caller, process) {
* @param {*} process
* @returns
*/
function getApps(user, process) {
async function getApps(user, process) {
if (
process.credentials &&
process.credentials.session &&
@ -331,10 +238,12 @@ function getApps(user, process) {
if (user) {
let db = new Database(user);
try {
let names = JSON.parse(db.get('apps'));
return Object.fromEntries(
names.map((name) => [name, db.get('path:' + name)])
);
let names = JSON.parse(await db.get('apps'));
let result = {};
for (let name of names) {
result[name] = await db.get('path:' + name);
}
return result;
} catch {}
}
return {};
@ -398,7 +307,6 @@ async function getProcessBlob(blobId, key, options) {
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = options.timeout;
process.stats = false;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@ -430,9 +338,9 @@ async function getProcessBlob(blobId, key, options) {
}
},
user: getUser(process, process),
users: function () {
users: async function () {
try {
return JSON.parse(new Database('auth').get('users'));
return JSON.parse(await new Database('auth').get('users'));
} catch {
return [];
}
@ -545,7 +453,7 @@ async function getProcessBlob(blobId, key, options) {
{
action: 'identities',
},
await getIdentityInfo(
await ssb.getIdentityInfo(
process?.credentials?.session?.name,
options?.packageOwner,
options?.packageName
@ -577,9 +485,10 @@ async function getProcessBlob(blobId, key, options) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
process.credentials.session.name &&
process.credentials.session.name !== 'guest'
) {
let id = ssb.createIdentity(process.credentials.session.name);
let id = await ssb.createIdentity(process.credentials.session.name);
await process.sendIdentities();
broadcastAppEventToUser(
process?.credentials?.session?.name,
@ -587,7 +496,7 @@ async function getProcessBlob(blobId, key, options) {
options.packageName,
'setActiveIdentity',
[
await getActiveIdentity(
await ssb.getActiveIdentity(
process.credentials?.session?.name,
options.packageOwner,
options.packageName
@ -595,6 +504,8 @@ async function getProcessBlob(blobId, key, options) {
]
);
return id;
} else {
throw new Error('Must be signed-in to create an account.');
}
};
if (process.credentials?.permissions?.administration) {
@ -610,31 +521,27 @@ async function getProcessBlob(blobId, key, options) {
imports.core.globalSettingsGet = function (key) {
return gGlobalSettings[key];
};
imports.core.globalSettingsSet = function (key, value) {
imports.core.globalSettingsSet = async function (key, value) {
print('Setting', key, value);
await loadSettings();
gGlobalSettings[key] = value;
setGlobalSettings(gGlobalSettings);
print('Done.');
};
imports.core.deleteUser = function (user) {
return Promise.resolve(
imports.core.permissionTest('delete_user')
).then(function () {
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
db.set('users', users);
}
});
imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user');
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = await db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
await db.set('users', users);
}
};
}
if (options.api) {
@ -696,7 +603,7 @@ async function getProcessBlob(blobId, key, options) {
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getActiveIdentity = () =>
getActiveIdentity(
ssb.getActiveIdentity(
process.credentials?.session?.name,
options.packageOwner,
options.packageName
@ -785,6 +692,9 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
imports.ssb.addEventListener = undefined;
imports.ssb.removeEventListener = undefined;
imports.ssb.getIdentityInfo = undefined;
imports.fetch = function (url, options) {
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
};
@ -856,7 +766,7 @@ async function getProcessBlob(blobId, key, options) {
};
process.task.setImports(imports);
process.task.activate();
let source = await getBlobOrContent(blobId);
let source = await ssb.blobGet(blobId);
let appSourceName = blobId;
let appSource = utf8Decode(source);
try {
@ -864,7 +774,7 @@ async function getProcessBlob(blobId, key, options) {
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await getBlobOrContent(id);
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
@ -874,7 +784,7 @@ async function getProcessBlob(blobId, key, options) {
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
f,
await getBlobOrContent(appObject.files[f]),
await ssb.blobGet(appObject.files[f]),
]);
})
);
@ -889,6 +799,10 @@ async function getProcessBlob(blobId, key, options) {
}
await process.task.execute({name: appSourceName, source: appSource});
resolveReady(process);
if (!gStatsTimer) {
gStatsTimer = true;
sendStats();
}
} catch (error) {
if (process.app) {
if (process?.task?.onError) {
@ -910,56 +824,15 @@ async function getProcessBlob(blobId, key, options) {
* @param {*} settings
* @returns
*/
function setGlobalSettings(settings) {
async function setGlobalSettings(settings) {
gGlobalSettings = settings;
try {
return new Database('core').set('settings', JSON.stringify(settings));
return await new Database('core').set('settings', JSON.stringify(settings));
} catch (error) {
print('Error storing settings:', error);
}
}
/**
* TODOC
* @param {*} data
* @param {*} bytes
* @returns
*/
function startsWithBytes(data, bytes) {
if (data.byteLength >= bytes.length) {
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
for (let i = 0; i < bytes.length; i++) {
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
return;
}
}
return true;
}
}
/**
* TODOC
* @param {*} path
* @returns
*/
function guessTypeFromName(path) {
let extension = path.split('.').pop();
return k_mime_types[extension];
}
/**
* TODOC
* @param {*} data
* @returns
*/
function guessTypeFromMagicBytes(data) {
for (let magic of k_magic_bytes) {
if (startsWithBytes(data, magic.bytes)) {
return magic.type;
}
}
}
/**
* TODOC
* @param {*} response
@ -975,7 +848,9 @@ function sendData(response, data, type, headers, status_code) {
Object.assign(
{
'Content-Type':
type || guessTypeFromMagicBytes(data) || 'application/binary',
type ||
httpd.mime_type_from_magic_bytes(data) ||
'application/binary',
'Content-Length': data.byteLength,
},
headers || {}
@ -997,21 +872,6 @@ function sendData(response, data, type, headers, status_code) {
}
}
/**
* TODOC
* @param {*} id
* @returns
*/
async function getBlobOrContent(id) {
if (!id) {
return;
} else if (id.startsWith('&')) {
return ssb.blobGet(id);
} else if (id.startsWith('%')) {
return ssb.messageContentGet(id);
}
}
let g_handler_index = 0;
/**
@ -1054,7 +914,7 @@ async function useAppHandler(
},
respond: do_resolve,
},
credentials: httpd.auth_query(headers),
credentials: await httpd.auth_query(headers),
packageOwner: packageOwner,
packageName: packageName,
}
@ -1079,34 +939,6 @@ async function useAppHandler(
* @returns
*/
async function blobHandler(request, response, blobId, uri) {
// TODO(tasiaiso): break this down ?
for (let i in k_static_files) {
if (uri === k_static_files[i].uri && k_static_files[i].path) {
let stat = await File.stat('core/' + k_static_files[i].path);
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile('core/' + k_static_files[i].path);
response.writeHead(
200,
Object.assign(
{
'Content-Type': k_static_files[i].type,
'Content-Length': data.byteLength,
etag: '"' + id + '"',
},
k_static_files[i].headers || {}
)
);
response.end(data);
}
return;
}
}
if (!uri) {
response.writeHead(303, {
Location:
@ -1139,7 +971,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(id);
data = await ssb.blobGet(id);
if (match[3]) {
let appObject = JSON.parse(data);
data = appObject.files[match[3]];
@ -1171,7 +1003,7 @@ async function blobHandler(request, response, blobId, uri) {
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(blobId);
data = await ssb.blobGet(blobId);
sendData(
response,
data,
@ -1185,7 +1017,7 @@ async function blobHandler(request, response, blobId, uri) {
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1];
let appName = match[2];
let credentials = httpd.auth_query(request.headers);
let credentials = await httpd.auth_query(request.headers);
if (
credentials &&
credentials.session &&
@ -1195,7 +1027,7 @@ async function blobHandler(request, response, blobId, uri) {
let database = new Database(user);
let app_object = JSON.parse(utf8Decode(request.body));
let previous_id = database.get('path:' + appName);
let previous_id = await database.get('path:' + appName);
if (previous_id) {
try {
let previous_object = JSON.parse(
@ -1216,7 +1048,7 @@ async function blobHandler(request, response, blobId, uri) {
let newBlobId = await ssb.blobStore(JSON.stringify(app_object));
let apps = new Set();
let apps_original = database.get('apps');
let apps_original = await database.get('apps');
try {
apps = new Set(JSON.parse(apps_original));
} catch {}
@ -1225,9 +1057,9 @@ async function blobHandler(request, response, blobId, uri) {
}
apps = JSON.stringify([...apps].sort());
if (apps != apps_original) {
database.set('apps', apps);
await database.set('apps', apps);
}
database.set('path:' + appName, newBlobId);
await database.set('path:' + appName, newBlobId);
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('/' + newBlobId);
} else {
@ -1248,7 +1080,7 @@ async function blobHandler(request, response, blobId, uri) {
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1];
let appName = match[2];
let credentials = https.auth_query(request.headers);
let credentials = await httpd.auth_query(request.headers);
if (
credentials &&
credentials.session &&
@ -1258,10 +1090,10 @@ async function blobHandler(request, response, blobId, uri) {
let database = new Database(user);
let apps = new Set();
try {
apps = new Set(JSON.parse(database.get('apps')));
apps = new Set(JSON.parse(await database.get('apps')));
} catch {}
if (apps.delete(appName)) {
database.set('apps', JSON.stringify([...apps].sort()));
await database.set('apps', JSON.stringify([...apps].sort()));
}
database.remove('path:' + appName);
} else {
@ -1287,9 +1119,9 @@ async function blobHandler(request, response, blobId, uri) {
app_id = await db.get('path:' + match[2]);
}
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
id = app_object.files[uri.substring(1)];
if (!id && app_object.files['handler.js']) {
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
id = app_object?.files[uri.substring(1)];
if (!id && app_object?.files['handler.js']) {
let answer;
try {
answer = await useAppHandler(
@ -1343,8 +1175,10 @@ async function blobHandler(request, response, blobId, uri) {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
};
data = await getBlobOrContent(id);
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
data = await ssb.blobGet(id);
let type =
httpd.mime_type_from_extension(uri) ||
httpd.mime_type_from_magic_bytes(data);
sendData(response, data, type, headers);
}
} else {
@ -1353,6 +1187,10 @@ async function blobHandler(request, response, blobId, uri) {
}
}
ssb.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]);
});
ssb.addEventListener('broadcasts', function () {
broadcastEvent('onBroadcastsChanged', []);
});
@ -1367,7 +1205,7 @@ ssb.addEventListener('connections', function () {
async function loadSettings() {
let data = {};
try {
let settings = new Database('core').get('settings');
let settings = await new Database('core').get('settings');
if (settings) {
data = JSON.parse(settings);
}
@ -1387,7 +1225,7 @@ async function loadSettings() {
*/
function sendStats() {
let apps = Object.values(gProcesses)
.filter((process) => process.app && process.stats)
.filter((process) => process.app)
.map((process) => process.app);
if (apps.length) {
let stats = getStats();
@ -1400,19 +1238,6 @@ function sendStats() {
}
}
/**
* TODOC
* @param {*} process
* @param {*} enabled
*/
function enableStats(process, enabled) {
process.stats = enabled;
if (!gStatsTimer) {
gStatsTimer = true;
sendStats();
}
}
/**
* TODOC
*/
@ -1424,34 +1249,7 @@ loadSettings()
httpd.all('/app/socket', app.socket);
httpd.all('', function default_http_handler(request, response) {
let match;
if (request.uri === '/' || request.uri === '') {
let host = request.headers['x-forwarded-host'] ?? request.headers.host;
try {
for (let line of (gGlobalSettings.index_map || '').split('\n')) {
let parts = line.split('=');
if (parts.length == 2 && host.match(new RegExp(parts[0], 'i'))) {
response.writeHead(303, {
Location:
(request.client.tls ? 'https://' : 'http://') +
host +
parts[1],
'Content-Length': '0',
});
return response.end();
}
}
} catch (e) {
print(e);
}
response.writeHead(303, {
Location:
(request.client.tls ? 'https://' : 'http://') +
host +
gGlobalSettings.index,
'Content-Length': '0',
});
return response.end();
} else if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
return blobHandler(request, response, match[1], match[2]);
} else if (
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
@ -1491,8 +1289,15 @@ loadSettings()
async function start_tls() {
const kCertificatePath = 'data/httpd/certificate.pem';
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
let certificate = utf8Decode(await File.readFile(kCertificatePath));
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);
@ -1546,57 +1351,4 @@ function storePermission(user, packageOwner, packageName, permission, allow) {
}
}
async function getActiveIdentity(user, packageOwner, packageName) {
if (user && packageOwner && packageName) {
let id = await new Database(user).get(`id:${packageOwner}:${packageName}`);
if (!id) {
let ids = await ssb.getIdentities(user);
if (ids) {
id = ids[0];
}
}
return id;
}
}
async function getIdentityInfo(user, packageOwner, packageName) {
let identities = await ssb.getIdentities(user);
let names = new Object();
for (let identity of identities) {
names[identity] = identity;
}
await ssb.sqlAsync(
`
SELECT author, 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
JOIN json_each(?) AS ids
ON messages.author = ids.value
WHERE json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL)
WHERE author_rank = 1
`,
[JSON.stringify(identities)],
function (row) {
names[row.author] = row.name;
}
);
return {
identities: identities,
identity: await getActiveIdentity(user, packageOwner, packageName),
names: names,
};
}
export {
gGlobalSettings as globalSettings,
setGlobalSettings,
enableStats,
invoke,
getSessionProcessBlob,
getActiveIdentity,
getIdentityInfo,
};
export {gGlobalSettings as globalSettings, invoke, getSessionProcessBlob};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 B

View File

@ -4,7 +4,7 @@
<title>Tilde Friends</title>
<link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
function set_access_key_title(event) {
@ -25,6 +25,17 @@
max-height: 100%;
"
>
<noscript>
<div class="w3-container">
<div class="w3-panel w3-red w3-padding w3-card-4">
<h1>TildeFriends requires JavaScript.</h1>
<p>
It looks like JavaScript is disabled or unsupported. This isn't
going to work.
</p>
</div>
</div>
</noscript>
<tf-navigation></tf-navigation>
<div id="content" class="hbox" style="flex: 1 0; overflow: auto">
<div

88
core/tildefriends.svg Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="65"
height="65"
viewBox="0 0 61 65"
fill="none"
version="1.1"
id="svg910"
sodipodi:docname="tildefriends.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs914" />
<sodipodi:namedview
id="namedview912"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="18.369231"
inkscape:cx="32.472781"
inkscape:cy="32.5"
inkscape:window-width="2256"
inkscape:window-height="1447"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg910" />
<path
style="fill:#0af;stroke-width:.712717;fill-opacity:1"
d="M6 0h49a8 8 45 0 1 8 8v49a8 8 135 0 1-8 8H6a8 8 45 0 1-8-8V8a8 8 135 0 1 8-8Z"
id="path886" />
<g
aria-label="~"
id="text890"
style="font-size:40px;line-height:1.25;fill:#000000">
<path
d="m 1.6762187,36.689095 v -4.003907 q 2.0703125,-2.34375 5.4296875,-2.34375 1.171875,0 2.4609375,0.351563 1.2890623,0.332031 3.6718753,1.347656 1.347656,0.566406 2.011718,0.742188 0.683594,0.175781 1.367188,0.175781 1.269531,0 2.617187,-0.761719 1.367188,-0.761719 2.421875,-1.914062 v 4.140625 q -1.25,1.171875 -2.539062,1.699218 -1.269531,0.527344 -2.871094,0.527344 -1.171875,0 -2.246094,-0.273437 -1.054687,-0.273438 -3.378906,-1.308594 -2.3046873,-1.035156 -3.847656,-1.035156 -1.25,0 -2.3632813,0.546875 -1.09375,0.527343 -2.734375,2.109375 z"
style="font-family:Arial;-inkscape-font-specification:'Arial, Normal'"
id="path1704" />
</g>
<g
transform="translate(16.213 5.975) scale(.72923)"
id="g896">
<circle
cx="36"
cy="36"
r="23"
fill="#fcea2b"
id="circle892" />
<path
fill="#3f3f3f"
d="M45.331 38.564c3.963 0 7.178-2.862 7.178-6.389 0-1.765.448-3.53-.852-4.685-1.299-1.156-4.345-1.704-6.326-1.704-2.357 0-5.143.143-6.451 1.704-.894 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389zM25.738 38.564c3.963 0 7.179-2.862 7.179-6.389 0-1.765.447-3.53-.852-4.685-1.3-1.156-4.345-1.704-6.327-1.704-2.356 0-5.142.143-6.451 1.704-.893 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389z"
id="path894" />
</g>
<g
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-miterlimit="10"
stroke-width="2"
transform="translate(16.213 5.975) scale(.72923)"
id="g908">
<circle
cx="35.887"
cy="36.056"
r="23"
id="circle898" />
<path
d="M45.702 44.862c-6.574 3.525-14.045 3.658-19.63 0M18.883 30.464s-.953 8.55 6.86 7.918c2.62-.212 7.817-.65 7.867-8.342.005-.698-.007-1.6-.81-2.63-1.065-1.367-3.572-1.971-9.945-1.422 0 0-3.446-.1-3.972 4.476z"
id="path900" />
<path
d="m18.953 29.931-.433-3.372 3.833-.527M52.741 30.464s.953 8.55-6.86 7.918c-2.62-.212-7.817-.65-7.868-8.342-.004-.698.008-1.6.811-2.63 1.065-1.367 3.572-1.971 9.945-1.422 0 0 3.446-.1 3.972 4.476z"
id="path902" />
<path
d="M31.505 26.416s4.124 2.534 8.657 0M33.536 31.318s2.202-3.751 4.536 0M52.664 29.933l.433-3.371-3.833-.528"
id="path904" />
<path
d="M33.955 30.027s1.795-3.75 3.699 0"
id="path906" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

66
default.nix Normal file
View File

@ -0,0 +1,66 @@
# How to upgrade to a newer version
# - Comment `src.hash`
# - Change `version`
# - Run `$ nix build`
# This will fetch the source code
# Since `hash` is not provided, nix will stop building and throw an error:
#
# error: hash mismatch in fixed-output derivation '/nix/store/fghi3ljs6fhz8pwm3dh73j5fwjpq5wbz-source.drv':
# specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
# got: sha256-+uthA1w8CmZfW+WOK9wYGl2fUl/k10ufOc8W+Pwa9iQ=
# error: 1 dependencies of derivation '/nix/store/imcwsw5r74vkd8r0qa2k7cys2xfgraaz-tildefriends-0.0.18.drv' failed to build
#
# - Change `src.hash` to the new one, ie `sha256-+uthA1w8CmZfW+WOK9wYGl2fUl/k10ufOc8W+Pwa9iQ=`
# - Uncomment `src.hash`
# - Build again, this time it should work.
# - Check the release notes, if there's a new dependency or a change to `GNUMakefile`, this file might need to be changed too.
# For more details, contact tasiaiso @ https://tilde.club/~tasiaiso/
{
pkgs ? import <nixpkgs> {},
lib ? import <nixpkgs/lib>,
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.22";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-Su+y++zVXmYNbwfhCP6w5e36oxW5fkURPFzFLjbyFEI=";
fetchSubmodules = true;
};
nativeBuildInputs = with pkgs; [
glibc
gnumake
openssl
which
];
buildInputs = with pkgs; [
glibc
openssl
which
];
buildPhase = ''
make -j $NIX_BUILD_CORES release
'';
installPhase = ''
mkdir -p $out/bin
cp -r out/release/tildefriends $out/bin
'';
doCheck = false;
meta = with pkgs; {
homepage = "https://tildefriends.net";
description = "Make apps and friends from the comfort of your web browser.";
mainProgram = "tildefriends";
license = with lib.licenses; [mit];
platforms = lib.platforms.all;
};
}

1
deps/c-ares vendored Submodule

@ -0,0 +1 @@
Subproject commit 27b98d96eff6122fb981e338bddef3d6a57d8d44

177
deps/c-ares_config/ares_build.h vendored Normal file
View File

@ -0,0 +1,177 @@
#ifndef __CARES_BUILD_H
#define __CARES_BUILD_H
/*
* Copyright (C) The c-ares project and its contributors
* SPDX-License-Identifier: MIT
*/
#define CARES_STATICLIB
#ifdef CARES_HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#ifdef CARES_HAVE_SYS_SOCKET_H
# include <sys/socket.h>
#endif
#ifdef CARES_HAVE_SYS_SELECT_H
# include <sys/select.h>
#endif
#if defined(_WIN32)
#undef HAVE_REGISTERWAITFORSINGLEOBJECT
#define CARES_HAVE_WINSOCK2_H
#define CARES_HAVE_WINDOWS_H
#define CARES_HAVE_WS2TCPIP_H
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#define CARES_TYPEOF_ARES_SOCKLEN_T int
#define CARES_TYPEOF_ARES_SSIZE_T ssize_t
#else
#define CARES_TYPEOF_ARES_SOCKLEN_T socklen_t
#define CARES_TYPEOF_ARES_SSIZE_T ssize_t
#endif
#if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
#define GETSERVBYNAME_R_ARGS 6
#define GETSERVBYPORT_R_ARGS 6
#define HAVE_GETSERVBYNAME_R 1
#define HAVE_GETSERVBYPORT_R 1
#endif
#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
#define HAVE_PIPE2 1
#endif
#if defined(__OpenBSD__) || defined(__HAIKU__)
#define GETSERVBYNAME_R_ARGS 4
#define GETSERVBYPORT_R_ARGS 4
#endif
#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__OpenBSD__) && !defined(__HAIKU__)
#define HAVE_MALLOC_H 1
#define HAVE_EPOLL 1
#define HAVE_SYS_EPOLL_H 1
#define HAVE_SYS_RANDOM_H 1
#endif
#if !defined(__WIN32)
#undef AC_APPLE_UNIVERSAL_BUILD
#undef ETC_INET
#define GETHOSTNAME_TYPE_ARG2 size_t
#define GETNAMEINFO_QUAL_ARG1
#define GETNAMEINFO_TYPE_ARG1 struct sockaddr *
#define GETNAMEINFO_TYPE_ARG2 socklen_t
#define GETNAMEINFO_TYPE_ARG46 socklen_t
#define GETNAMEINFO_TYPE_ARG7 int
#define HAVE_AF_INET6 1
#define HAVE_ARPA_INET_H 1
#define HAVE_ARPA_NAMESER_COMPAT_H 1
#define HAVE_ARPA_NAMESER_H 1
#define HAVE_ASSERT_H 1
#define HAVE_CLOCK_GETTIME_MONOTONIC 1
#define HAVE_CONNECT 1
#define HAVE_DLFCN_H 1
#define HAVE_ERRNO_H 1
#define HAVE_POLL_H 1
#define HAVE_POLL 1
#define HAVE_PIPE 1
#define HAVE_FCNTL 1
#define HAVE_FCNTL_H 1
#define HAVE_FCNTL_O_NONBLOCK 1
#define HAVE_FREEADDRINFO 1
#define HAVE_GETADDRINFO 1
#define HAVE_GETENV 1
#define HAVE_GETHOSTNAME 1
#define HAVE_GETNAMEINFO 1
#if !defined(__HAIKU__)
#define HAVE_GETRANDOM 1
#endif
#define HAVE_GETTIMEOFDAY 1
#define HAVE_IF_INDEXTONAME 1
#define HAVE_IF_NAMETOINDEX 1
#define HAVE_INET_NTOP 1
#define HAVE_INET_PTON 1
#define HAVE_INTTYPES_H 1
#define HAVE_IOCTL 1
#define HAVE_IOCTL_FIONBIO 1
#define HAVE_IOCTL_SIOCGIFADDR 1
#define HAVE_LIMITS_H 1
#define HAVE_LONGLONG 1
#define HAVE_MEMORY_H 1
#define HAVE_MSG_NOSIGNAL 1
#define HAVE_NETDB_H 1
#define HAVE_NETINET_IN_H 1
#define HAVE_NETINET_TCP_H 1
#define HAVE_NET_IF_H 1
#define HAVE_PF_INET6 1
#define HAVE_RECV 1
#define HAVE_RECVFROM 1
#define HAVE_SEND 1
#define HAVE_SETSOCKOPT 1
#define HAVE_SIGNAL_H 1
#define HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID 1
#define HAVE_SOCKET 1
#define HAVE_STDBOOL_H 1
#define HAVE_STDINT_H 1
#define HAVE_STDLIB_H 1
#define HAVE_STRCASECMP 1
#define HAVE_STRDUP 1
#define HAVE_STRINGS_H 1
#define HAVE_STRING_H 1
#define HAVE_STRNCASECMP 1
#define HAVE_STRUCT_ADDRINFO 1
#define HAVE_STRUCT_IN6_ADDR 1
#define HAVE_STRUCT_SOCKADDR_IN6 1
#define HAVE_STRUCT_SOCKADDR_STORAGE 1
#define HAVE_STRUCT_TIMEVAL 1
#define HAVE_SYS_IOCTL_H 1
#define HAVE_SYS_PARAM_H 1
#define HAVE_SYS_SELECT_H 1
#define HAVE_SYS_SOCKET_H 1
#define HAVE_SYS_STAT_H 1
#define HAVE_SYS_TIME_H 1
#define HAVE_SYS_TYPES_H 1
#define HAVE_SYS_UIO_H 1
#define HAVE_TIME_H 1
#define HAVE_IFADDRS_H 1
#define HAVE_UNISTD_H 1
#define HAVE_WRITEV 1
#if defined(__ANDROID__) || defined(__APPLE__) || defined(__OpenBSD__)
#define HAVE_ARC4RANDOM_BUF 1
#else
#undef HAVE_ARC4RANDOM_BUF
#endif
#define HAVE_GETIFADDRS 1
#define HAVE_STAT 1
#define CARES_RANDOM_FILE "/dev/urandom"
#define RECVFROM_QUAL_ARG5
#define RECVFROM_TYPE_ARG1 int
#define RECVFROM_TYPE_ARG2 void *
#define RECVFROM_TYPE_ARG2_IS_VOID 0
#define RECVFROM_TYPE_ARG3 size_t
#define RECVFROM_TYPE_ARG4 int
#define RECVFROM_TYPE_ARG5 struct sockaddr *
#define RECVFROM_TYPE_ARG5_IS_VOID 0
#define RECVFROM_TYPE_ARG6 socklen_t *
#define RECVFROM_TYPE_ARG6_IS_VOID 0
#define RECVFROM_TYPE_RETV ssize_t
#define RECV_TYPE_ARG1 int
#define RECV_TYPE_ARG2 void *
#define RECV_TYPE_ARG3 size_t
#define RECV_TYPE_ARG4 int
#define RECV_TYPE_RETV ssize_t
#define SEND_TYPE_ARG1 int
#define SEND_TYPE_ARG2 const void *
#define SEND_TYPE_ARG3 size_t
#define SEND_TYPE_ARG4 int
#define SEND_TYPE_RETV ssize_t
#undef USE_BLOCKING_SOCKETS
#undef WIN32_LEAN_AND_MEAN
#define HAVE_PTHREAD_H 1
#define CARES_THREADS 1
#endif
#endif /* __CARES_BUILD_H */

File diff suppressed because one or more lines are too long

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

@ -19,9 +19,10 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.0.tgz",
"integrity": "sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==",
"version": "6.18.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
"integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@ -36,32 +37,35 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz",
"integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.2.tgz",
"integrity": "sha512-Fq7eWOl1Rcbrfn6jD8FPCj9Auaxdm5nIK5RYOeW7ughnd/rY5AmPg6b+CfsG39ZHdwiwe8lde3q8uR7CF5S0yQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.0.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz",
"integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.0.0"
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
"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",
@ -78,6 +82,7 @@
"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",
@ -92,15 +97,17 @@
"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.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
"integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@ -111,9 +118,10 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz",
"integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==",
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
"integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@ -124,6 +132,7 @@
"version": "6.5.6",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@ -133,12 +142,14 @@
"node_modules/@codemirror/state": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==",
"license": "MIT"
},
"node_modules/@codemirror/theme-one-dark": {
"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",
@ -147,9 +158,10 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.26.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz",
"integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==",
"version": "6.33.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.33.0.tgz",
"integrity": "sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0",
@ -161,6 +173,7 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -175,6 +188,7 @@
"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"
}
@ -184,6 +198,7 @@
"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"
}
@ -193,22 +208,25 @@
"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"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
"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"
},
"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"
@ -217,12 +235,14 @@
"node_modules/@lezer/common": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz",
"integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==",
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -230,17 +250,19 @@
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
"integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
"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"
}
},
"node_modules/@lezer/html": {
"version": "1.3.9",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.9.tgz",
"integrity": "sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==",
"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",
@ -248,9 +270,10 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.14.tgz",
"integrity": "sha512-GEdUyspTRgc5dwIGebUk+f3BekvqEWVIYsIuAC3pA8e8wcikGwBZRWRa450L0s8noGWuULwnmi4yjxTnYz9PpA==",
"version": "1.4.18",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.18.tgz",
"integrity": "sha512-Y8BeHOt4LtcxJgXwadtfSeWPrh0XzklcCHnCVT+vOsxqH4gWmunP2ykX+VVOlM/dusyVyiNfG3lv0f10UK+mgA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@ -261,6 +284,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -268,9 +292,10 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"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"
}
@ -279,6 +304,7 @@
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@ -304,6 +330,7 @@
"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",
@ -325,6 +352,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@ -343,212 +371,231 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz",
"integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz",
"integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz",
"integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz",
"integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz",
"integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz",
"integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz",
"integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz",
"integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz",
"integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz",
"integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz",
"integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz",
"integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz",
"integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz",
"integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz",
"integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz",
"integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"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"
},
"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=="
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -560,12 +607,14 @@
"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
"dev": true,
"license": "MIT"
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
@ -577,6 +626,7 @@
"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",
@ -591,17 +641,20 @@
"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
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"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"
}
@ -609,13 +662,15 @@
"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=="
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"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"
@ -628,6 +683,7 @@
"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"
}
@ -636,6 +692,7 @@
"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"
},
@ -647,6 +704,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"license": "MIT",
"dependencies": {
"builtin-modules": "^3.3.0"
},
@ -658,11 +716,15 @@
}
},
"node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.0"
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -671,17 +733,20 @@
"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=="
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
"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=="
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
@ -694,6 +759,7 @@
"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"
}
@ -702,6 +768,7 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@ -715,9 +782,10 @@
}
},
"node_modules/rollup": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz",
"integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
},
@ -729,25 +797,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.3",
"@rollup/rollup-android-arm64": "4.14.3",
"@rollup/rollup-darwin-arm64": "4.14.3",
"@rollup/rollup-darwin-x64": "4.14.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.3",
"@rollup/rollup-linux-arm-musleabihf": "4.14.3",
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
"@rollup/rollup-linux-arm64-musl": "4.14.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.3",
"@rollup/rollup-linux-riscv64-gnu": "4.14.3",
"@rollup/rollup-linux-s390x-gnu": "4.14.3",
"@rollup/rollup-linux-x64-gnu": "4.14.3",
"@rollup/rollup-linux-x64-musl": "4.14.3",
"@rollup/rollup-win32-arm64-msvc": "4.14.3",
"@rollup/rollup-win32-ia32-msvc": "4.14.3",
"@rollup/rollup-win32-x64-msvc": "4.14.3",
"@rollup/rollup-android-arm-eabi": "4.21.3",
"@rollup/rollup-android-arm64": "4.21.3",
"@rollup/rollup-darwin-arm64": "4.21.3",
"@rollup/rollup-darwin-x64": "4.21.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
"@rollup/rollup-linux-arm-musleabihf": "4.21.3",
"@rollup/rollup-linux-arm64-gnu": "4.21.3",
"@rollup/rollup-linux-arm64-musl": "4.21.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
"@rollup/rollup-linux-riscv64-gnu": "4.21.3",
"@rollup/rollup-linux-s390x-gnu": "4.21.3",
"@rollup/rollup-linux-x64-gnu": "4.21.3",
"@rollup/rollup-linux-x64-musl": "4.21.3",
"@rollup/rollup-win32-arm64-msvc": "4.21.3",
"@rollup/rollup-win32-ia32-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.21.3",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -766,13 +840,15 @@
"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"
}
@ -781,13 +857,15 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true
"dev": true,
"license": "MIT"
},
"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"
}
@ -797,6 +875,7 @@
"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"
@ -805,12 +884,14 @@
"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=="
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"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"
},
@ -819,10 +900,11 @@
}
},
"node_modules/terser": {
"version": "5.30.3",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz",
"integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==",
"version": "5.33.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz",
"integrity": "sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -839,7 +921,8 @@
"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=="
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
}
}
}

2
deps/libbacktrace vendored

@ -1 +1 @@
Subproject commit 7ead8c1ea2f4aeafe9c5b9ef8a9461a9ba781aa8
Subproject commit 86885d14049fab06ef8a33aac51664230ca09200

2
deps/libsodium vendored

@ -1 +1 @@
Subproject commit fb4533b0a941b3a5b1db5687d1b008a5853d1f29
Subproject commit 0217d07326f0ffbe79d6ce09793843e135a67487

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
deps/openssl_src vendored Submodule

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

1715
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

8613
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

97
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.45.3"
#define SQLITE_VERSION_NUMBER 3045003
#define SQLITE_SOURCE_ID "2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355"
#define SQLITE_VERSION "3.46.1"
#define SQLITE_VERSION_NUMBER 3046001
#define SQLITE_SOURCE_ID "2024-08-13 09:16:08 c9c2ab54ba1f5f46360f1b4f35d849cd3f080e6fc2b6c60e91b16c63f69a1e33"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -764,11 +764,11 @@ struct sqlite3_file {
** </ul>
** xLock() upgrades the database file lock. In other words, xLock() moves the
** database file lock in the direction NONE toward EXCLUSIVE. The argument to
** xLock() is always on of SHARED, RESERVED, PENDING, or EXCLUSIVE, never
** xLock() is always one of SHARED, RESERVED, PENDING, or EXCLUSIVE, never
** SQLITE_LOCK_NONE. If the database file lock is already at or above the
** requested lock, then the call to xLock() is a no-op.
** xUnlock() downgrades the database file lock to either SHARED or NONE.
* If the lock is already at or below the requested lock state, then the call
** If the lock is already at or below the requested lock state, then the call
** to xUnlock() is a no-op.
** The xCheckReservedLock() method checks whether any database connection,
** either in this process or in some other process, is holding a RESERVED,
@ -3305,8 +3305,8 @@ SQLITE_API int sqlite3_set_authorizer(
#define SQLITE_RECURSIVE 33 /* NULL NULL */
/*
** CAPI3REF: Tracing And Profiling Functions
** METHOD: sqlite3
** CAPI3REF: Deprecated Tracing And Profiling Functions
** DEPRECATED
**
** These routines are deprecated. Use the [sqlite3_trace_v2()] interface
** instead of the routines described here.
@ -6887,6 +6887,12 @@ SQLITE_API int sqlite3_autovacuum_pages(
** The exceptions defined in this paragraph might change in a future
** release of SQLite.
**
** Whether the update hook is invoked before or after the
** corresponding change is currently unspecified and may differ
** depending on the type of change. Do not rely on the order of the
** hook call with regards to the final result of the operation which
** triggers the hook.
**
** The update hook implementation must not do anything that will modify
** the database connection that invoked the update hook. Any actions
** to modify the database connection must be deferred until after the
@ -8357,7 +8363,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** The sqlite3_keyword_count() interface returns the number of distinct
** keywords understood by SQLite.
**
** The sqlite3_keyword_name(N,Z,L) interface finds the N-th keyword and
** The sqlite3_keyword_name(N,Z,L) interface finds the 0-based N-th keyword and
** makes *Z point to that keyword expressed as UTF8 and writes the number
** of bytes in the keyword into *L. The string that *Z points to is not
** zero-terminated. The sqlite3_keyword_name(N,Z,L) routine returns
@ -9936,24 +9942,45 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** <li value="2"><p>
** ^(If the sqlite3_vtab_distinct() interface returns 2, that means
** that the query planner does not need the rows returned in any particular
** order, as long as rows with the same values in all "aOrderBy" columns
** are adjacent.)^ ^(Furthermore, only a single row for each particular
** combination of values in the columns identified by the "aOrderBy" field
** needs to be returned.)^ ^It is always ok for two or more rows with the same
** values in all "aOrderBy" columns to be returned, as long as all such rows
** are adjacent. ^The virtual table may, if it chooses, omit extra rows
** that have the same value for all columns identified by "aOrderBy".
** ^However omitting the extra rows is optional.
** order, as long as rows with the same values in all columns identified
** by "aOrderBy" are adjacent.)^ ^(Furthermore, when two or more rows
** contain the same values for all columns identified by "colUsed", all but
** one such row may optionally be omitted from the result.)^
** The virtual table is not required to omit rows that are duplicates
** over the "colUsed" columns, but if the virtual table can do that without
** too much extra effort, it could potentially help the query to run faster.
** This mode is used for a DISTINCT query.
** <li value="3"><p>
** ^(If the sqlite3_vtab_distinct() interface returns 3, that means
** that the query planner needs only distinct rows but it does need the
** rows to be sorted.)^ ^The virtual table implementation is free to omit
** rows that are identical in all aOrderBy columns, if it wants to, but
** it is not required to omit any rows. This mode is used for queries
** ^(If the sqlite3_vtab_distinct() interface returns 3, that means the
** virtual table must return rows in the order defined by "aOrderBy" as
** if the sqlite3_vtab_distinct() interface had returned 0. However if
** two or more rows in the result have the same values for all columns
** identified by "colUsed", then all but one such row may optionally be
** omitted.)^ Like when the return value is 2, the virtual table
** is not required to omit rows that are duplicates over the "colUsed"
** columns, but if the virtual table can do that without
** too much extra effort, it could potentially help the query to run faster.
** This mode is used for queries
** that have both DISTINCT and ORDER BY clauses.
** </ol>
**
** <p>The following table summarizes the conditions under which the
** virtual table is allowed to set the "orderByConsumed" flag based on
** the value returned by sqlite3_vtab_distinct(). This table is a
** restatement of the previous four paragraphs:
**
** <table border=1 cellspacing=0 cellpadding=10 width="90%">
** <tr>
** <td valign="top">sqlite3_vtab_distinct() return value
** <td valign="top">Rows are returned in aOrderBy order
** <td valign="top">Rows with the same value in all aOrderBy columns are adjacent
** <td valign="top">Duplicates over all colUsed columns may be omitted
** <tr><td>0<td>yes<td>yes<td>no
** <tr><td>1<td>no<td>yes<td>no
** <tr><td>2<td>no<td>yes<td>yes
** <tr><td>3<td>yes<td>yes<td>yes
** </table>
**
** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS"
@ -11998,6 +12025,30 @@ SQLITE_API int sqlite3changegroup_schema(sqlite3_changegroup*, sqlite3*, const c
*/
SQLITE_API int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData);
/*
** CAPI3REF: Add A Single Change To A Changegroup
** METHOD: sqlite3_changegroup
**
** This function adds the single change currently indicated by the iterator
** passed as the second argument to the changegroup object. The rules for
** adding the change are just as described for [sqlite3changegroup_add()].
**
** If the change is successfully added to the changegroup, SQLITE_OK is
** returned. Otherwise, an SQLite error code is returned.
**
** The iterator must point to a valid entry when this function is called.
** If it does not, SQLITE_ERROR is returned and no change is added to the
** changegroup. Additionally, the iterator must not have been opened with
** the SQLITE_CHANGESETAPPLY_INVERT flag. In this case SQLITE_ERROR is also
** returned.
*/
SQLITE_API int sqlite3changegroup_add_change(
sqlite3_changegroup*,
sqlite3_changeset_iter*
);
/*
** CAPI3REF: Obtain A Composite Changeset From A Changegroup
** METHOD: sqlite3_changegroup
@ -12802,8 +12853,8 @@ struct Fts5PhraseIter {
** EXTENSION API FUNCTIONS
**
** xUserData(pFts):
** Return a copy of the context pointer the extension function was
** registered with.
** Return a copy of the pUserData pointer passed to the xCreateFunction()
** API when the extension function was registered.
**
** xColumnTotalSize(pFts, iCol, pnToken):
** If parameter iCol is less than zero, set output variable *pnToken

63
docs/cheatsheet.md Normal file
View File

@ -0,0 +1,63 @@
# Tilde Friends 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

@ -1,209 +1,166 @@
# Philosophy
# Tilde Friends Developer's Guide
Tilde Friends is a platform for making, running, and sharing web applications.
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.
When you visit Tilde Friends in a web browser, you are presented with a
terminal interface, typically with a big text output box covering most of the
page and an input box at the bottom, into which text or commands can be
entered. A script runs to produce text output and consume user input.
## Example 1: Hello, world!
The script is a Tilde Friends application, and it runs on the server, which
means that unlike client-side JavaScript, it can have the ability to read and
write files on the server or create network connections to other machines.
Unlike node.js or other server-side runtime environments, applications are
limited for security reasons to not interfere with each other or bring the
entire server down.
Of course we must start with a classic.
Above the terminal, an "Edit" link brings a visitor to the source code for the
current Tilde Friends application, which they can then edit, save as their own,
and run.
### app.js
# Architecture
```
app.setDocument('<h1 style="color: #fff">Hello, world!</h1>');
```
Tilde Friends is a C++ application with a JavaScript runtime that provides
restricted access to filesystem, network, and other system resources. The core
process runs a core set of scripts that implement a web server, typically
starting a new process for each visitor's session which runs scripts for the
active application and stopping it when the visitor leaves.
### Output
Only the core process has access to most system resources, but session
processes can be given accesss through the core process.
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Hello, world!&lt;/h1&gt;"></iframe>
Service processes are identical to session processes, but they are not tied to
a user session.
### Explanation
## Communication
At a glance, this might seem mundane, but for it to work:
In the same way that web browsers expose APIs for scripts running in the
browser to modify the document, play sounds and video, and draw, Tilde Friends
exposes APIs for scripts running on a Tilde Friends server to interact with a
visitor's web browser, read and write files on the server, and otherwise
interact with the world.
- 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
There are several distinct classes of APIs.
But you don't have to think about all that. Call a function, and you see the result.
First, there are low-level functions exposed from C++ to JavaScript. Most of
these are only available to the core process. These typically only go through
a basic JavaScript to C++ transition and are relatively fast and immediate.
## Example 2: Hit Counter
// Displays some text to the server's console.
print("Hello, world!");
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.
There is a mechanism for communicating between processes. Functions can be
exported and called across process boundaries. When this is done, any
arguments are serialized to a network protocol, deserialized by the other
process, the function called, and finally any return value is passed back in
the same way. Any functions referenced by the arguments or return value are
also exported and can be subsequently called across process boundaries.
Functions called across process boundaries are always asynchronous, returning a
Promise. Care must be taken for security reasons to not pass dangerous
functions ("deleteAllMydata()") to untrusted processes, and it is best for
performance reasons to minimize the data size transferred between processes.
### app.js
// Send an "add" function to any other running processes. When called, it
// will run in this process.
core.broadcast({add: function(x, y) { return x + y; }});
```
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>
`);
}
// Receive the above message and call the function.
core.register("onMessage", function(sender, message) {
message.add(3, 4).then(x => terminal.print(x.toString()));
});
main();
```
Finally, there is a core web interface that runs on the client's browser that
extends access to a running Tilde Friends script.
### Output
// Displays a message in the client's browser.
terminal.print("Hello, world!");
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Welcome, visitor #1!&lt;/h1&gt;"></iframe>
## API Documentation
### Explanation
The Tilde Friends API is very much evolving.
Just as pure browser apps have access to `localStorage`, Tilde Friends apps have access to key-value storage on the server.
All currently registered methods can be explored in the
[documentation](https://www.tildefriends.net/~cory/documentation) app.
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.
All browser-facing methods are implemented in [client.js](core/client.js).
Most process-related methods are implemented in [core.js](core/core.js).
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.
Higher-level behaviors are often implemented within library-style apps
themselves and are beyond the scope of this document.
## Example 3: Files
### Terminal
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.
All interaction with a human user is through a terminal-like interface. Though
it is somewhat limiting, it makes simple things easy, and it is possible to
construct complicated interfaces by creating and interacting with an iframe.
### app.js
#### terminal.print(arguments...)
```
async function main() {
let html = utf8Decode(await getFile('index.html'));
app.setDocument(html);
}
Print to the terminal. Arguments and lists are recursively expanded. Numerous
special values are supported as implemented in client.cs.
main();
```
// Create a link.
terminal.print({href: "http://www.tildefriends.net/", value: "Tilde Friends!"});
### index.html
// Create an iframe.
terminal.print({iframe: "&lt;b&gt;Hello, world!&lt;/b&gt;", width: 640, height: 480});
```
<html>
<head>
<script type="module" src="script.js"></script>
</head>
<body style="color: #fff">
<h1>File Test</h1>
</body>
</html>
```
// Use style.
terminal.print({style: "color: #f00", value: "Hello, world!"});
### script.js
// Create a link that when clicked will act as if the user typed a command.
terminal.print({command: "exit", value: "Get out of here."});
```
window.addEventListener('load', function() {
document.body.appendChild(document.createTextNode('Hello, world');
});
```
#### terminal.clear()
### Output
Clears the terminal output.
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>File Test</h1>Hello, world!&lt;/body&gt;"></iframe>
#### terminal.readLine()
### Explanation
Read a line of input from the user.
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.
#### terminal.setEcho(echo)
## Example 4: Remote Procedure Call
Controls whether the terminal will automatically echo user input. Defaults to true.
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.
#### terminal.setPrompt(prompt)
### app.js
Sets the terminal prompt. The default is "&gt;".
```
import * as tf from '/tfrpc.js';
#### terminal.setTitle(title)
function sum() {
let s = 0
for (let x of arguments) {
s += x;
}
return s;
}
tf.register(sum);
Sets the browser window/tab title.
async function main() {
app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
```
#### terminal.split(terminalList)
### index.html
Reconfigures the terminal layout, potentially into multiple split panes.
```
<html>
<body>
<h1 id='result'>Calculating...</h1>
</body>
<script type="module" src="script.js"></script>
</html>
```
terminal.split([
{
type: "horizontal",
children: [
{name: "left", basis: "2in", grow: 0, shrink: 0},
{name: "middle", grow: 1},
{name: "right", basis: "2in", grow: 0, shrink: 0},
],
},
]);
### script.js
#### terminal.select(name)
```
import * as tf from '/static/tfrpc.js';
Directs subsequent output to the named terminal.
window.addEventListener('load', async function() {
document.getElementById('result').innerText = await tf.rpc.sum(1, 2, 3);
});
```
#### terminal.postMessageToIframe(iframeName, message)
### Output
Sends a message to the iframe that was created with the given name, using the
browser's window.postMessage.
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>6</h1>&lt;/body&gt;"></iframe>
### Database
### Explanation
Tilde Friends uses lmdb as a basic key value store. Keys and values are all
expected to be of type String. Each application gets its own isolated
database.
Here the browser makes an asynchronous call to the server to do some basic math and update its DOM with the result.
#### database.get(key)
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.
Retrieve the database value associated with the given key.
## Conclusion
#### database.set(key, value)
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.
Sets the database value for the given key, overwriting any existing value.
#### database.remove(key)
Remove the database entry for the given key.
#### database.getAlll()
Retrieve a list of all key names.
### Network
Network access is generally not extended to untrusted users.
It is necessary to grant network permissions to an app owner through the
administration app.
Apps that require network access must declare it like this:
//! { "permissions": ["network"] }
#### network.newConnection()
Creates a Connection object.
#### connection.connect(host, port)
Opens a TCP connection to host:port.
#### connection.read(readCallback)
Begins reading and calls readCallback(data) for all data received.
#### connection.write(data)
Writes data to the connection.
#### connection.close()
Closes the connection.
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.

64
docs/vision.md Normal file
View File

@ -0,0 +1,64 @@
# Tilde Friends 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.

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717281328,
"narHash": "sha256-evZPzpf59oNcDUXxh2GHcxHkTEG4fjae2ytWP85jXRo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3b2b28c1daa04fe2ae47c21bb76fd226eac4ca1",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

39
flake.nix Normal file
View File

@ -0,0 +1,39 @@
{
description = "Tilde Friends is a platform for making, running, and sharing web applications.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
};
in rec
{
# Nix formatter, run using `$ nix fmt`
formatter = pkgs.alejandra;
# Exports the tildefriends package
# Build with `$ nix build`
packages.default = pkgs.callPackage ./default.nix {};
# Creates a shell with the necessary dependencies
# Enter using `$ nix develop`
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
openssl
llvmPackages_17.clang-unwrapped
unzip
doxygen
graphviz
];
};
});
}

View File

@ -0,0 +1,7 @@
* Reworked Android to launch its sandbox processes the prescribed way so that
a Google Play-shippable android .aab is possible.
* Reworked the build to generate what F-Droid wants, too.
* Updated libbacktrace.
* Updated CodeMirror.
* Updated Android NDK.
* Enabled LTO for smaller code sizes.

View File

@ -0,0 +1,12 @@
* Took an initial whack at encouraging internet-based discovery of open peers.
* Added settings to control whether replication, room, peer exchange, and account registration are allowed.
* Implemented prompt() on Android.
* Fixed some incorrect cross-thread use of the main JS context.
* Fixed yet another incorrect use of the DB from the main thread, from an RPC that isn't ever hit. Hmm.
* Minor admin layout fixes.
* Added c-ares for TXT record lookups.
* Latest libsodium-stable.
* Latest libbacktrace.
* Latest CodeMirror.
* Updated to Lit 3.2.0.
* Updated sqlite to 3.46.1.

View File

@ -0,0 +1,3 @@
Tilde Friends is both a peer-to-peer social network client, participating in
Secure Scuttlebutt, as well as a platform for writing and running web
applications.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -0,0 +1 @@
A tool for making and sharing

1
metadata/en-US/title.txt Normal file
View File

@ -0,0 +1 @@
Tilde Friends

View File

@ -1,18 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends"
android:versionCode="18"
android:versionName="0.0.18">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
android:versionCode="27"
android:versionName="0.0.23-wip">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="Tilde Friends"
android:usesCleartextTraffic="true"
android:debuggable="true"
android:extractNativeLibs="true">
android:usesCleartextTraffic="true">
<meta-data android:name="android.max_aspect" android:value="2.1"/>
<activity
android:name=".MainActivity"
android:name=".TildeFriendsActivity"
android:icon="@drawable/icon"
android:configChanges="orientation|screenSize"
android:exported="true">
@ -21,5 +19,10 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name=".TildeFriendsSandboxService"
android:exported="false"
android:isolatedProcess="true"
android:process=":sandbox"/>
</application>
</manifest>

View File

@ -0,0 +1,5 @@
{
"optimizations" : {
"uncompress_native_libraries" : {}
}
}

6
src/android/build.xml Normal file
View File

@ -0,0 +1,6 @@
<project>
<target name="clean"></target>
<target name="release">
<echo>Creating ../../../out/apk/TildeFriends-release.fdroid.unsigned.apk for release.</echo>
</target>
</project>

View File

@ -3,15 +3,20 @@ package com.unprompted.tildefriends;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Environment;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.SystemClock;
import android.os.strictmode.Violation;
import android.text.InputType;
import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent;
@ -21,26 +26,22 @@ import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.URLUtil;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
//import android.webkit.WebView;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.lang.Process;
import java.lang.Thread;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
@ -49,18 +50,31 @@ import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit;
public class MainActivity extends Activity {
WebView web_view;
public class TildeFriendsActivity extends Activity {
static TildeFriendsActivity s_activity;
TildeFriendsWebView web_view;
String base_url;
Process process;
String port_file_path;
Thread thread;
Thread server_thread;
ServiceConnection service_connection;
private ValueCallback<Uri[]> upload_message;
private final static int FILECHOOSER_RESULT = 1;
private float touch_down_y;
static {
Log.w("tildefriends", "Calling system.loadLibrary().");
System.loadLibrary("tildefriends");
Log.w("tildefriends", "system.loadLibrary() completed.");
}
public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path, ConnectivityManager connectivity_manager);
public static native int tf_sandbox_main(int pipe_fd);
@Override
protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.penaltyLog()
@ -69,17 +83,17 @@ public class MainActivity extends Activity {
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
web_view = (WebView)findViewById(R.id.web);
web_view = (TildeFriendsWebView)findViewById(R.id.web);
set_status("Extracting executable...");
Log.w("tildefriends", String.format("getFilesDir() is %s", getFilesDir().toString()));
Log.w("tildefriends", String.format("getPackageResourcePath() is %s", getPackageResourcePath().toString()));
Log.w("tildefriends", String.format("nativeLibraryDir is %s", getApplicationInfo().nativeLibraryDir));
String port_file_path = getFilesDir().toString() + "/port.txt";
port_file_path = getFilesDir().toString() + "/port.txt";
new File(port_file_path).delete();
base_url = "http://127.0.0.1:12345/";
MainActivity activity = this;
TildeFriendsActivity activity = this;
thread = new Thread(new Runnable() {
@Override
@ -133,22 +147,26 @@ public class MainActivity extends Activity {
thread.start();
set_status("Starting server...");
String exe = getApplicationInfo().nativeLibraryDir + "/tildefriends.so";
ProcessBuilder builder = new ProcessBuilder(exe, "run", "-z", getPackageResourcePath().toString(), "-a", "out_http_port_file=" + port_file_path, "-p", "0");
Log.w("tildefriends", "files = " + getFilesDir().toString());
Log.w("tildefriends", "exe = " + exe);
builder.directory(getFilesDir());
builder.inheritIO();
try {
process = builder.start();
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException starting process: " + e.toString());
}
server_thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Calling tf_server_main.");
int result = tf_server_main(
getFilesDir().toString(),
getPackageResourcePath().toString(),
port_file_path,
(ConnectivityManager)getApplicationContext().getSystemService(CONNECTIVITY_SERVICE));
Log.w("tildefriends", "tf_server_main returned " + result + ".");
}
});
server_thread.start();
web_view.getSettings().setJavaScriptEnabled(true);
web_view.getSettings().setDatabaseEnabled(true);
web_view.getSettings().setDomStorageEnabled(true);
set_database_path();
web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) {
Log.w("tildefriends", "Let's download: " + url + " (" + content_disposition + ")");
@ -182,7 +200,25 @@ public class MainActivity extends Activity {
});
web_view.setWebChromeClient(new WebChromeClient() {
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
@ -205,20 +241,51 @@ public class MainActivity extends Activity {
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(defaultValue);
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
/*
** https://stackoverflow.com/questions/5907369/file-upload-in-webview
** https://stackoverflow.com/questions/8586691/how-to-open-file-save-dialog-in-android
*/
@Override
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> message, WebChromeClient.FileChooserParams params) {
upload_message = message;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
MainActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), MainActivity.FILECHOOSER_RESULT);
TildeFriendsActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), TildeFriendsActivity.FILECHOOSER_RESULT);
return true;
}
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d("tildefriends", consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
@ -245,13 +312,8 @@ public class MainActivity extends Activity {
@Override
protected void onDestroy()
{
if (process != null) {
Log.w("tildefriends", "Killing process.");
process.destroyForcibly();
Log.w("tildefriends", "Process killed.");
process = null;
}
super.onDestroy();
s_activity = null;
}
@Override
@ -353,4 +415,57 @@ public class MainActivity extends Activity {
web_view.setVisibility(View.VISIBLE);
text_view.setVisibility(View.GONE);
}
public static void start_sandbox(int pipe_fd) {
Log.w("tildefriends", "starting service with fd: " + pipe_fd);
Intent intent = new Intent(s_activity, TildeFriendsSandboxService.class);
s_activity.service_connection = new ServiceConnection() {
@Override
public void onBindingDied(ComponentName name) {
Log.w("tildefriends", "onBindingDied");
}
@Override
public void onNullBinding(ComponentName name) {
Log.w("tildefriends", "onNullBinding");
}
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
Log.w("tildefriends", "onServiceConnected");
Parcel data = Parcel.obtain();
ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(pipe_fd);
data.writeParcelable(pfd, 0);
try {
binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) {
Log.w("tildefriends", "RemoteException");
} finally {
data.recycle();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.w("tildefriends", "onServiceDisconnected");
}
};
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE);
}
public static void stop_sandbox() {
Log.w("tildefriends", "stop_sandbox");
if (s_activity.service_connection != null) {
s_activity.unbindService(s_activity.service_connection);
s_activity.service_connection = null;
}
}
@SuppressWarnings("deprecation")
private void set_database_path()
{
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
web_view.getSettings().setDatabasePath(getDatabasePath("webview").getPath());
}
}
}

View File

@ -0,0 +1,59 @@
package com.unprompted.tildefriends;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.util.Log;
public class TildeFriendsSandboxService extends Service {
public static final int START_CALL = IBinder.FIRST_CALL_TRANSACTION;
Thread thread;
public int onStartCommand(Intent intent, int flags, int start_id) {
Log.w("tildefriends", "TildeFriendsSandboxService: onStartCommand");
return super.onStartCommand(intent, flags, start_id);
}
public void onDestroy() {
Log.w("tildefriends", "TildeFriendsSandboxService: onDestroy");
super.onDestroy();
}
private void start_thread(int pipe_fd) {
thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Calling tf_sandbox_main.");
int result = TildeFriendsActivity.tf_sandbox_main(pipe_fd);
Log.w("tildefriends", "tf_sandbox_main returned " + result + ".");
}
});
thread.start();
}
@Override
public IBinder onBind(Intent intent) {
return new Binder() {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
if (code == START_CALL) {
ParcelFileDescriptor pfd = data.readParcelable(ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
if (pfd != null) {
Log.w("tildefriends", "fd is " + pfd.getFd());
start_thread(pfd.detachFd());
try {
pfd.close();
} catch (java.io.IOException e) {
}
}
return true;
}
return false;
}
};
}
}

View File

@ -2,16 +2,15 @@ package com.unprompted.tildefriends;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
public class WebView extends android.webkit.WebView {
public class TildeFriendsWebView extends android.webkit.WebView {
boolean overscrolledY = false;
public WebView(final Context context) {
public TildeFriendsWebView(final Context context) {
super(context);
}
public WebView(final Context context, final AttributeSet attrs) {
public TildeFriendsWebView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}

View File

@ -1,78 +1,55 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
android:width="65dp"
android:height="65dp"
android:viewportWidth="61"
android:viewportHeight="65">
<path
android:pathData="M36,36m-23,0a23,23 0,1 1,46 0a23,23 0,1 1,-46 0"
android:fillColor="#FCEA2B"/>
android:pathData="M6,0h49a8,8 45,0 1,8 8v49a8,8 135,0 1,-8 8H6a8,8 45,0 1,-8 -8V8a8,8 135,0 1,8 -8Z"
android:strokeWidth=".712717"
android:fillColor="#0af"
android:fillAlpha="1"/>
<path
android:pathData="M45.331,38.564c3.963,0 7.178,-2.862 7.178,-6.389c0,-1.765 0.447,-3.529 -0.852,-4.685s-4.345,-1.704 -6.326,-1.704c-2.357,0 -5.143,0.143 -6.451,1.704c-0.893,1.065 -0.727,3.253 -0.727,4.685C38.153,35.702 41.366,38.564 45.331,38.564z"
android:fillColor="#3F3F3F"/>
android:pathData="m1.6762,36.6891v-4.0039q2.0703,-2.3438 5.4297,-2.3438 1.1719,0 2.4609,0.3516 1.2891,0.332 3.6719,1.3477 1.3477,0.5664 2.0117,0.7422 0.6836,0.1758 1.3672,0.1758 1.2695,0 2.6172,-0.7617 1.3672,-0.7617 2.4219,-1.9141v4.1406q-1.25,1.1719 -2.5391,1.6992 -1.2695,0.5273 -2.8711,0.5273 -1.1719,0 -2.2461,-0.2734 -1.0547,-0.2734 -3.3789,-1.3086 -2.3047,-1.0352 -3.8477,-1.0352 -1.25,0 -2.3633,0.5469 -1.0938,0.5273 -2.7344,2.1094z"
android:fillColor="#000000"/>
<path
android:pathData="M25.738,38.564c3.963,0 7.178,-2.862 7.178,-6.389c0,-1.765 0.447,-3.529 -0.852,-4.685s-4.345,-1.704 -6.326,-1.704c-2.357,0 -5.143,0.143 -6.451,1.704c-0.893,1.065 -0.727,3.253 -0.727,4.685C18.56,35.702 21.773,38.564 25.738,38.564z"
android:fillColor="#3F3F3F"/>
android:pathData="M42.4653,32.2273m-16.7723,0a16.7723,16.7723 0,1 1,33.5446 0a16.7723,16.7723 0,1 1,-33.5446 0"
android:fillColor="#fcea2b"/>
<path
android:pathData="M35.887,36.056m-23,0a23,23 0,1 1,46 0a23,23 0,1 1,-46 0"
android:pathData="M49.2697,34.097c2.8899,0 5.2344,-2.0871 5.2344,-4.6591 0,-1.2871 0.3267,-2.5742 -0.6213,-3.4164 -0.9473,-0.843 -3.1685,-1.2426 -4.6131,-1.2426 -1.7188,0 -3.7504,0.1043 -4.7043,1.2426 -0.6519,0.7766 -0.5302,2.3722 -0.5302,3.4164 0,2.572 2.343,4.6591 5.2344,4.6591zM34.9819,34.097c2.8899,0 5.2351,-2.0871 5.2351,-4.6591 0,-1.2871 0.326,-2.5742 -0.6213,-3.4164 -0.948,-0.843 -3.1685,-1.2426 -4.6138,-1.2426 -1.7181,0 -3.7497,0.1043 -4.7043,1.2426 -0.6512,0.7766 -0.5302,2.3722 -0.5302,3.4164 0,2.572 2.343,4.6591 5.2344,4.6591z"
android:fillColor="#3f3f3f"/>
<path
android:pathData="M42.3829,32.2681m-16.7723,0a16.7723,16.7723 0,1 1,33.5446 0a16.7723,16.7723 0,1 1,-33.5446 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M45.702,44.862c-6.574,3.525 -14.045,3.658 -19.629,0"
android:pathData="M49.5403,38.6897c-4.794,2.5705 -10.242,2.6675 -14.3148,0M29.983,28.1903s-0.695,6.2349 5.0025,5.774c1.9106,-0.1546 5.7004,-0.474 5.7369,-6.0832 0.0036,-0.509 -0.0051,-1.1668 -0.5907,-1.9179 -0.7766,-0.9969 -2.6048,-1.4373 -7.2522,-1.037 0,0 -2.5129,-0.0729 -2.8965,3.264z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M18.883,30.464c0,0 -0.953,8.551 6.861,7.918c2.62,-0.212 7.816,-0.651 7.867,-8.343c0.005,-0.698 -0.008,-1.599 -0.811,-2.63c-1.065,-1.367 -3.572,-1.971 -9.945,-1.422C22.855,25.988 19.409,25.889 18.883,30.464z"
android:pathData="m30.0341,27.8016 l-0.3158,-2.459 2.7951,-0.3843M54.6733,28.1903s0.695,6.2349 -5.0025,5.774c-1.9106,-0.1546 -5.7004,-0.474 -5.7376,-6.0832 -0.0029,-0.509 0.0058,-1.1668 0.5914,-1.9179 0.7766,-0.9969 2.6048,-1.4373 7.2522,-1.037 0,0 2.5129,-0.0729 2.8965,3.264z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M18.953,29.931l-0.433,-3.371l3.833,-0.528"
android:pathData="M39.1874,25.2383s3.0073,1.8479 6.3129,0M40.6685,28.813s1.6058,-2.7353 3.3078,0M54.6172,27.803l0.3158,-2.4582 -2.7951,-0.385"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M52.741,30.464c0,0 0.953,8.551 -6.861,7.918c-2.62,-0.212 -7.816,-0.651 -7.867,-8.343c-0.005,-0.698 0.008,-1.599 0.811,-2.63c1.065,-1.367 3.572,-1.971 9.945,-1.422C48.769,25.988 52.215,25.889 52.741,30.464z"
android:pathData="M40.974,27.8716s1.309,-2.7346 2.6974,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M31.505,26.416c0,0 4.124,2.534 8.657,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M33.536,31.318c0,0 2.202,-3.751 4.536,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M52.664,29.933l0.433,-3.371l-3.833,-0.528"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M33.955,30.027c0,0 1.795,-3.751 3.699,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.unprompted.tildefriends.WebView
<com.unprompted.tildefriends.TildeFriendsWebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@ -4,6 +4,7 @@
#include "mem.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include "sqlite3.h"
@ -50,7 +51,7 @@ void tf_database_register(JSContext* context)
JS_SetPropertyStr(context, global, "Database", constructor);
JSValue databases = JS_NewObject(context);
JS_SetPropertyStr(context, global, "databases", databases);
JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 0, 0, 0, NULL));
JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 1, 0, 0, NULL));
JS_FreeValue(context, global);
}
@ -91,159 +92,455 @@ static void _database_finalizer(JSRuntime* runtime, JSValue value)
--_database_count;
}
typedef struct _database_get_t
{
const char* id;
const char* key;
size_t key_length;
char* out_value;
size_t out_length;
JSValue promise[2];
} database_get_t;
static void _database_get_work(tf_ssb_t* ssb, void* user_data)
{
database_get_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW)
{
size_t length = sqlite3_column_bytes(statement, 0);
char* data = tf_malloc(length + 1);
memcpy(data, sqlite3_column_text(statement, 0), length);
data[length] = '\0';
work->out_value = data;
work->out_length = length;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->out_value)
{
result = JS_NewStringLen(context, work->out_value, work->out_length);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->out_value);
tf_free(work);
}
static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue entry = JS_UNDEFINED;
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
{
size_t length;
const char* keyString = JS_ToCStringLen(context, &length, argv[0]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW)
{
entry = JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0));
}
JS_FreeCString(context, keyString);
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
size_t length;
const char* key = JS_ToCStringLen(context, &length, argv[0]);
database_get_t* work = tf_malloc(sizeof(database_get_t) + strlen(database->id) + 1 + length + 1);
*work = (database_get_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.key_length = length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, length + 1);
JS_FreeCString(context, key);
tf_ssb_run_work(ssb, _database_get_work, _database_get_after_work, work);
result = JS_NewPromiseCapability(context, work->promise);
}
return entry;
return result;
}
typedef struct _database_set_t
{
const char* id;
const char* key;
size_t key_length;
const char* value;
size_t value_length;
bool result;
JSValue promise[2];
} database_set_t;
static void _database_set_work(tf_ssb_t* ssb, void* user_data)
{
database_set_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{
work->result = true;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _database_set_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_set_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK)
size_t key_length = 0;
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
size_t value_length = 0;
const char* value = JS_ToCStringLen(context, &value_length, argv[1]);
database_set_t* work = tf_malloc(sizeof(database_set_t) + strlen(database->id) + 1 + key_length + 1 + value_length + 1);
*work = (database_set_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.value = (const char*)(work + 1) + strlen(database->id) + 1 + key_length + 1,
.key_length = key_length,
.value_length = value_length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, key_length + 1);
memcpy((char*)work->value, value, value_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_set_work, _database_set_after_work, work);
JS_FreeCString(context, key);
JS_FreeCString(context, value);
}
return result;
}
typedef struct _database_exchange_t
{
const char* id;
const char* key;
size_t key_length;
const char* expected;
size_t expected_length;
const char* value;
size_t value_length;
bool result;
JSValue promise[2];
} database_exchange_t;
static void _database_exchange_work(tf_ssb_t* ssb, void* user_data)
{
database_exchange_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement;
if (!work->expected)
{
if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
{
size_t keyLength;
const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]);
size_t valueLength;
const char* valueString = JS_ToCStringLen(context, &valueLength, argv[1]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, valueString, valueLength, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK)
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{
work->result = sqlite3_changes(db) != 0;
}
JS_FreeCString(context, keyString);
JS_FreeCString(context, valueString);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
return JS_UNDEFINED;
else if (sqlite3_prepare(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 4, work->expected, work->expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{
work->result = sqlite3_changes(db) != 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _database_exchange_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_exchange_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = work->result ? JS_TRUE : JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeCString(context, work->key);
JS_FreeCString(context, work->expected);
JS_FreeCString(context, work->value);
tf_free((char*)work->id);
tf_free(work);
}
static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue exchanged = JS_UNDEFINED;
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1]))
{
if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK)
{
size_t key_length;
size_t set_length;
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, key_length, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, set, set_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE)
{
exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE;
}
JS_FreeCString(context, key);
JS_FreeCString(context, set);
sqlite3_finalize(statement);
}
}
else if (sqlite3_prepare(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK)
{
size_t key_length;
size_t expected_length;
size_t set_length;
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
const char* expected = JS_ToCStringLen(context, &expected_length, argv[1]);
const char* set = JS_ToCStringLen(context, &set_length, argv[2]);
if (sqlite3_bind_text(statement, 1, set, set_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, database->id, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, key, key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 4, expected, expected_length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE)
{
exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE;
}
JS_FreeCString(context, key);
JS_FreeCString(context, expected);
JS_FreeCString(context, set);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
database_exchange_t* work = tf_malloc(sizeof(database_exchange_t));
*work = (database_exchange_t) {
.id = tf_strdup(database->id),
};
work->key = JS_ToCStringLen(context, &work->key_length, argv[0]);
work->expected = (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) ? NULL : JS_ToCStringLen(context, &work->expected_length, argv[1]);
work->value = JS_ToCStringLen(context, &work->value_length, argv[2]);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_exchange_work, _database_exchange_after_work, work);
}
return exchanged;
return result;
}
typedef struct _database_remove_t
{
const char* id;
size_t key_length;
JSValue promise[2];
char key[];
} database_remove_t;
static void _database_remove_work(tf_ssb_t* ssb, void* user_data)
{
database_remove_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_OK)
{
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
}
static void _database_remove_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_remove_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((char*)work->id);
tf_free(work);
}
static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK)
{
size_t keyLength;
const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_OK)
{
}
JS_FreeCString(context, keyString);
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
size_t key_length = 0;
const char* key = JS_ToCStringLen(context, &key_length, argv[0]);
database_remove_t* work = tf_malloc(sizeof(database_remove_t) + key_length + 1);
*work = (database_remove_t) {
.id = tf_strdup(database->id),
.key_length = key_length,
};
memcpy(work->key, key, key_length + 1);
JS_FreeCString(context, key);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(tf_task_get_ssb(database->task), _database_remove_work, _database_remove_after_work, work);
}
return JS_UNDEFINED;
return result;
}
typedef struct _database_get_all_t
{
const char* id;
const char* key;
size_t key_length;
char** out_values;
size_t* out_lengths;
int out_values_length;
JSValue promise[2];
} database_get_all_t;
static void _database_get_all_work(tf_ssb_t* ssb, void* user_data)
{
database_get_all_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key FROM properties WHERE id = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
work->out_values = tf_resize_vec(work->out_values, sizeof(char*) * (work->out_values_length + 1));
work->out_lengths = tf_resize_vec(work->out_lengths, sizeof(size_t) * (work->out_values_length + 1));
size_t length = sqlite3_column_bytes(statement, 0);
char* data = tf_malloc(length + 1);
memcpy(data, sqlite3_column_text(statement, 0), length);
data[length] = '\0';
work->out_values[work->out_values_length] = data;
work->out_lengths[work->out_values_length] = length;
work->out_values_length++;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_all_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_all_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
;
for (int i = 0; i < work->out_values_length; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewStringLen(context, work->out_values[i], work->out_lengths[i]));
tf_free((void*)work->out_values[i]);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->out_values);
tf_free(work->out_lengths);
tf_free(work);
}
static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue array = JS_UNDEFINED;
JSValue result = JS_UNDEFINED;
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ?1", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK)
{
array = JS_NewArray(context);
uint32_t index = 0;
while (sqlite3_step(statement) == SQLITE_ROW)
{
JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)));
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
size_t length;
const char* key = JS_ToCStringLen(context, &length, argv[0]);
database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1 + length + 1);
*work = (database_get_all_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.key_length = length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, length + 1);
JS_FreeCString(context, key);
tf_ssb_run_work(ssb, _database_get_all_work, _database_get_all_after_work, work);
result = JS_NewPromiseCapability(context, work->promise);
}
return array;
return result;
}
typedef struct _key_value_t
{
char* key;
size_t key_length;
char* value;
size_t value_length;
} key_value_t;
typedef struct _database_get_like_t
{
const char* id;
const char* pattern;
key_value_t* results;
int results_length;
JSValue promise[2];
} database_get_like_t;
static void _database_get_like_work(tf_ssb_t* ssb, void* user_data)
{
database_get_like_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->pattern, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
work->results = tf_resize_vec(work->results, sizeof(key_value_t) * (work->results_length + 1));
key_value_t* out = &work->results[work->results_length];
*out = (key_value_t) {
.key_length = sqlite3_column_bytes(statement, 0),
.value_length = sqlite3_column_bytes(statement, 1),
};
out->key = tf_malloc(out->key_length + 1);
memcpy(out->key, sqlite3_column_text(statement, 0), out->key_length + 1);
out->value = tf_malloc(out->value_length + 1);
memcpy(out->value, sqlite3_column_text(statement, 1), out->value_length + 1);
work->results_length++;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
static void _database_get_like_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
database_get_like_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewObject(context);
for (int i = 0; i < work->results_length; i++)
{
const key_value_t* row = &work->results[i];
JS_SetPropertyStr(context, result, row->key, JS_NewStringLen(context, row->value, row->value_length));
tf_free(row->key);
tf_free(row->value);
}
JS_FreeCString(context, work->pattern);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free((void*)work->id);
tf_free(work->results);
tf_free(work);
}
static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
@ -252,51 +549,77 @@ static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int
database_t* database = JS_GetOpaque(this_val, _database_class_id);
if (database)
{
sqlite3_stmt* statement;
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
const char* pattern = JS_ToCString(context, argv[0]);
if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, pattern, -1, NULL) == SQLITE_OK)
{
result = JS_NewObject(context);
while (sqlite3_step(statement) == SQLITE_ROW)
{
JS_SetPropertyStr(context, result, (const char*)sqlite3_column_text(statement, 0),
JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 1), sqlite3_column_bytes(statement, 1)));
}
}
JS_FreeCString(context, pattern);
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
database_get_like_t* work = tf_malloc(sizeof(database_get_like_t));
*work = (database_get_like_t) {
.id = tf_strdup(database->id),
.pattern = JS_ToCString(context, argv[0]),
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _database_get_like_work, _database_get_like_after_work, work);
}
return result;
}
typedef struct _databases_list_t
{
const char* pattern;
char** names;
int names_length;
JSValue promise[2];
} databases_list_t;
static void _databases_list_work(tf_ssb_t* ssb, void* user_data)
{
databases_list_t* work = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->pattern, -1, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
work->names = tf_resize_vec(work->names, sizeof(char*) * (work->names_length + 1));
work->names[work->names_length] = tf_strdup((const char*)sqlite3_column_text(statement, 0));
work->names_length++;
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
}
static void _databases_list_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
databases_list_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_NewArray(context);
for (int i = 0; i < work->names_length; i++)
{
JS_SetPropertyUint32(context, result, i, JS_NewString(context, work->names[i]));
tf_free(work->names[i]);
}
JS_FreeCString(context, work->pattern);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work->names);
tf_free(work);
}
static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
JSValue array = JS_UNDEFINED;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK)
{
const char* pattern = JS_ToCString(context, argv[0]);
if (sqlite3_bind_text(statement, 1, pattern, -1, NULL) == SQLITE_OK)
{
array = JS_NewArray(context);
uint32_t index = 0;
while (sqlite3_step(statement) == SQLITE_ROW)
{
JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)));
}
}
JS_FreeCString(context, pattern);
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return array;
databases_list_t* work = tf_malloc(sizeof(databases_list_t));
*work = (databases_list_t) {
.pattern = JS_ToCString(context, argv[0]),
};
JSValue result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _databases_list_work, _databases_list_after_work, work);
return result;
}

View File

@ -78,7 +78,7 @@ static void _file_write_write_callback(uv_fs_t* req)
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
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)
@ -91,6 +91,7 @@ static void _file_write_write_callback(uv_fs_t* req)
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);
@ -102,7 +103,8 @@ static void _file_write_open_callback(uv_fs_t* req)
int result = uv_fs_write(req->loop, req, fsreq->file, &buf, 1, 0, _file_write_write_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(result)));
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)
{
@ -114,9 +116,9 @@ static void _file_write_open_callback(uv_fs_t* req)
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
uv_fs_req_cleanup(req);
tf_free(req);
}
tf_free((void*)path);
}
static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
@ -156,7 +158,7 @@ static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int a
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, "%s", uv_strerror(result)));
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;
@ -164,7 +166,6 @@ static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int a
static void _file_read_read_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);
@ -177,8 +178,9 @@ static void _file_read_read_callback(uv_fs_t* req)
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to read %s: %s", req->path, uv_strerror(req->result)));
}
uv_fs_req_cleanup(req);
int result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
@ -189,6 +191,7 @@ static void _file_read_read_callback(uv_fs_t* req)
static void _file_read_open_callback(uv_fs_t* req)
{
const char* path = tf_strdup(req->path);
uv_fs_req_cleanup(req);
fs_req_t* fsreq = (fs_req_t*)req;
tf_task_t* task = req->loop->data;
@ -201,7 +204,7 @@ static void _file_read_open_callback(uv_fs_t* req)
int result = uv_fs_read(req->loop, req, fsreq->file, &buf, 1, 0, _file_read_read_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(result)));
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to read %s: %s", path, uv_strerror(result)));
result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
@ -212,10 +215,11 @@ static void _file_read_open_callback(uv_fs_t* req)
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for read: %s", path, uv_strerror(req->result)));
uv_fs_req_cleanup(req);
tf_free(req);
}
tf_free((void*)path);
}
static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
@ -238,7 +242,7 @@ static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int ar
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, file_name, UV_FS_O_RDONLY, 0, _file_read_open_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(result)));
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for read: %s", file_name, uv_strerror(result)));
uv_fs_req_cleanup(&req->fs);
tf_free(req);
}
@ -322,7 +326,7 @@ static void _file_read_file_zip_after_work(uv_work_t* work, int status)
}
else
{
tf_task_reject_promise(data->task, data->promise, JS_ThrowInternalError(data->context, "Error: %d.", data->result));
tf_task_reject_promise(data->task, data->promise, JS_ThrowInternalError(data->context, "Failed to read %s: %d.", data->file_path, data->result));
}
tf_free(data->buffer);
tf_free((void*)data->file_path);
@ -352,7 +356,7 @@ static JSValue _file_read_file_zip(JSContext* context, JSValueConst this_val, in
int r = uv_queue_work(tf_task_get_loop(task), &work->request, _file_read_file_zip_work, _file_read_file_zip_after_work);
if (r)
{
tf_task_reject_promise(task, work->promise, JS_ThrowInternalError(context, "%s", uv_strerror(r)));
tf_task_reject_promise(task, work->promise, JS_ThrowInternalError(context, "Failed to create read work for %s: %s", file_name, uv_strerror(r)));
tf_free((void*)work->file_path);
tf_free(work);
}

View File

@ -67,6 +67,7 @@ typedef struct _tf_http_connection_t
typedef struct _tf_http_handler_t
{
const char* pattern;
bool is_wildcard;
tf_http_callback_t* callback;
tf_http_cleanup_t* cleanup;
void* user_data;
@ -127,12 +128,48 @@ static void _http_allocate_buffer(uv_handle_t* handle, size_t suggested_size, uv
*buf = uv_buf_init(connection->incoming, sizeof(connection->incoming));
}
static bool _http_pattern_matches(const char* pattern, const char* path, bool is_wildcard)
{
if (!pattern || !*pattern || (!is_wildcard && strcmp(path, pattern) == 0))
{
return true;
}
if (is_wildcard)
{
int i = 0;
int j = 0;
while (pattern[i] && path[j] && pattern[i] != '*' && pattern[i] == path[j])
{
i++;
j++;
}
if (pattern[i] == '*')
{
while (true)
{
if (_http_pattern_matches(pattern + i + 1, path + j, strchr(pattern + i + 1, '*') != NULL))
{
return true;
}
if (!path[j])
{
break;
}
j++;
}
}
return !pattern[i] && !path[j];
}
return false;
}
static bool _http_find_handler(tf_http_t* http, const char* path, tf_http_callback_t** out_callback, const char** out_trace_name, void** out_user_data)
{
for (int i = 0; i < http->handlers_count; i++)
{
if (!http->handlers[i].pattern || !*http->handlers[i].pattern || strcmp(path, http->handlers[i].pattern) == 0 ||
(*http->handlers[i].pattern && strncmp(path, http->handlers[i].pattern, strlen(http->handlers[i].pattern)) == 0 && path[strlen(http->handlers[i].pattern) - 1] == '/'))
if (_http_pattern_matches(http->handlers[i].pattern, path, http->handlers[i].is_wildcard))
{
*out_callback = http->handlers[i].callback;
*out_trace_name = http->handlers[i].pattern;
@ -694,6 +731,7 @@ void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_
http->handlers = tf_resize_vec(http->handlers, sizeof(tf_http_handler_t) * (http->handlers_count + 1));
http->handlers[http->handlers_count++] = (tf_http_handler_t) {
.pattern = tf_strdup(pattern),
.is_wildcard = pattern && strchr(pattern, '*') != NULL,
.callback = callback,
.cleanup = cleanup,
.user_data = user_data,
@ -981,7 +1019,7 @@ void tf_http_request_unref(tf_http_request_t* request)
tf_free(request);
}
if (--connection->ref_count == 0)
if (connection && --connection->ref_count == 0)
{
if (connection->http->is_shutting_down)
{
@ -1033,7 +1071,7 @@ void* tf_http_get_user_data(tf_http_t* http)
const char* tf_http_get_cookie(const char* cookie_header, const char* name)
{
if (!cookie_header)
if (!cookie_header || !name)
{
return NULL;
}

View File

@ -31,11 +31,15 @@
#define tf_countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#define CYAN "\e[1;36m"
#define MAGENTA "\e[1;35m"
#define RESET "\e[0m"
const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
static JSValue _authenticate_jwt(JSContext* context, const char* jwt);
static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name);
static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
static JSClassID _httpd_class_id;
@ -326,11 +330,11 @@ static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_va
tf_ssb_t* ssb = tf_task_get_ssb(tf_task_get(context));
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
JSValue jwt = _authenticate_jwt(context, session);
JSValue jwt = _authenticate_jwt(ssb, context, session);
tf_free((void*)session);
JSValue name = !JS_IsUndefined(jwt) ? JS_GetPropertyStr(context, jwt, "name") : JS_UNDEFINED;
const char* name_string = !JS_IsUndefined(name) ? JS_ToCString(context, name) : NULL;
const char* session_token = _make_session_jwt(ssb, name_string);
const char* session_token = _make_session_jwt(tf_ssb_get_context(ssb), ssb, name_string);
const char* cookie = _make_set_session_cookie_header(request, session_token);
tf_free((void*)session_token);
JS_FreeCString(context, name_string);
@ -416,6 +420,7 @@ static JSValue _httpd_endpoint_start(JSContext* context, JSValueConst this_val,
*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 " is now up at " MAGENTA "http%s://127.0.0.1:%d/" RESET ".\n", tls ? "s" : "", assigned_port);
return JS_NewInt32(context, assigned_port);
}
@ -441,6 +446,55 @@ static JSValue _httpd_set_http_redirect(JSContext* context, JSValueConst this_va
return JS_UNDEFINED;
}
typedef struct _auth_query_work_t
{
const char* settings;
JSValue entry;
JSValue result;
JSValue promise[2];
} auth_query_work_t;
static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
{
auth_query_work_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
auth_query_work_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue name = JS_GetPropertyStr(context, work->entry, "name");
const char* name_string = JS_ToCString(context, name);
JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->result, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++)
{
JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
const char* permission_string = JS_ToCString(context, permission);
JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
JS_FreeCString(context, permission_string);
JS_FreeValue(context, permission);
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &work->result);
JS_FreeValue(context, work->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, user_permissions);
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)work->settings);
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
tf_free(work);
}
static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_task_t* task = tf_task_get(context);
@ -454,7 +508,7 @@ static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int
JSValue cookie = JS_GetPropertyStr(context, headers, "cookie");
const char* cookie_string = JS_ToCString(context, cookie);
const char* session = tf_http_get_cookie(cookie_string, "session");
JSValue entry = _authenticate_jwt(context, session);
JSValue entry = _authenticate_jwt(ssb, context, session);
tf_free((void*)session);
JS_FreeCString(context, cookie_string);
JS_FreeValue(context, cookie);
@ -462,37 +516,165 @@ static JSValue _httpd_auth_query(JSContext* context, JSValueConst this_val, int
JSValue result = JS_UNDEFINED;
if (!JS_IsUndefined(entry))
{
result = JS_NewObject(context);
JS_SetPropertyStr(context, result, "session", entry);
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, result, "permissions", out_permissions);
JSValue value = JS_NewObject(context);
JS_SetPropertyStr(context, value, "session", entry);
JSValue name = JS_GetPropertyStr(context, entry, "name");
const char* name_string = JS_ToCString(context, name);
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++)
{
JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
const char* permission_string = JS_ToCString(context, permission);
JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
JS_FreeCString(context, permission_string);
JS_FreeValue(context, permission);
}
JS_FreeValue(context, user_permissions);
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
auth_query_work_t* work = tf_malloc(sizeof(auth_query_work_t));
*work = (auth_query_work_t) {
.entry = entry,
.result = value,
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}
return result;
}
typedef struct _magic_bytes_t
{
const char* type;
uint8_t bytes[12];
uint8_t ignore[12];
} magic_bytes_t;
static bool _magic_bytes_match(const magic_bytes_t* magic, const uint8_t* actual, size_t size)
{
if (size < sizeof(magic->bytes))
{
return false;
}
int length = (int)tf_min(sizeof(magic->bytes), size);
for (int i = 0; i < length; i++)
{
if ((magic->bytes[i] & ~magic->ignore[i]) != (actual[i] & ~magic->ignore[i]))
{
return false;
}
}
return true;
}
static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
size_t size = 0;
uint8_t* bytes = tf_util_try_get_array_buffer(context, &size, argv[0]);
if (bytes)
{
const magic_bytes_t k_magic_bytes[] = {
{
.type = "image/jpeg",
.bytes = { 0xff, 0xd8, 0xff, 0xdb },
},
{
.type = "image/jpeg",
.bytes = { 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01 },
},
{
.type = "image/jpeg",
.bytes = { 0xff, 0xd8, 0xff, 0xee },
},
{
.type = "image/jpeg",
.bytes = { 0xff, 0xd8, 0xff, 0xe1, 0x00, 0x00, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 },
.ignore = { 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
},
{
.type = "image/png",
.bytes = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a },
},
{
.type = "image/gif",
.bytes = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 },
},
{
.type = "image/gif",
.bytes = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 },
},
{
.type = "image/webp",
.bytes = { 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50 },
.ignore = { 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 },
},
{
.type = "image/svg+xml",
.bytes = { 0x3c, 0x73, 0x76, 0x67 },
},
{
.type = "audio/mpeg",
.bytes = { 0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32 },
.ignore = { 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
},
{
.type = "video/mp4",
.bytes = { 0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d },
.ignore = { 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
},
{
.type = "video/mp4",
.bytes = { 0x00, 0x00, 0x00, 0x00, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32 },
.ignore = { 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
},
{
.type = "audio/midi",
.bytes = { 0x4d, 0x54, 0x68, 0x64 },
},
};
for (int i = 0; i < tf_countof(k_magic_bytes); i++)
{
if (_magic_bytes_match(&k_magic_bytes[i], bytes, size))
{
result = JS_NewString(context, k_magic_bytes[i].type);
break;
}
}
}
return result;
}
static const char* _ext_to_content_type(const char* ext, bool use_fallback)
{
if (ext)
{
typedef struct _ext_type_t
{
const char* ext;
const char* type;
} ext_type_t;
const ext_type_t k_types[] = {
{ .ext = ".html", .type = "text/html; charset=UTF-8" },
{ .ext = ".js", .type = "text/javascript; charset=UTF-8" },
{ .ext = ".mjs", .type = "text/javascript; charset=UTF-8" },
{ .ext = ".css", .type = "text/css; charset=UTF-8" },
{ .ext = ".png", .type = "image/png" },
{ .ext = ".json", .type = "application/json" },
{ .ext = ".map", .type = "application/json" },
{ .ext = ".svg", .type = "image/svg+xml" },
};
for (int i = 0; i < tf_countof(k_types); i++)
{
if (strcmp(ext, k_types[i].ext) == 0)
{
return k_types[i].type;
}
}
}
return use_fallback ? "application/binary" : NULL;
}
static JSValue _httpd_mime_type_from_extension(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
const char* name = JS_ToCString(context, argv[0]);
const char* type = _ext_to_content_type(strrchr(name, '.'), false);
JS_FreeCString(context, name);
return type ? JS_NewString(context, type) : JS_UNDEFINED;
}
static void _httpd_finalizer(JSRuntime* runtime, JSValue value)
{
tf_http_t* http = JS_GetOpaque(value, _httpd_class_id);
@ -622,26 +804,6 @@ typedef struct _http_file_t
char etag[512];
} http_file_t;
static const char* _ext_to_content_type(const char* ext)
{
if (ext)
{
if (strcmp(ext, ".js") == 0 || strcmp(ext, ".mjs") == 0)
{
return "text/javascript; charset=UTF-8";
}
if (strcmp(ext, ".css") == 0)
{
return "text/css; charset=UTF-8";
}
else if (strcmp(ext, ".png") == 0)
{
return "image/png";
}
}
return "application/binary";
}
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;
@ -650,7 +812,7 @@ static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int r
{
if (strcmp(path, "core/tfrpc.js") == 0)
{
const char* content_type = _ext_to_content_type(strrchr(path, '.'));
const char* content_type = _ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
@ -663,7 +825,7 @@ static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int r
}
else
{
const char* content_type = _ext_to_content_type(strrchr(path, '.'));
const char* content_type = _ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
@ -722,7 +884,7 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
const char* k_static_files[] = {
"index.html",
"client.js",
"favicon.png",
"tildefriends.svg",
"jszip.min.js",
"style.css",
"tfrpc.js",
@ -743,9 +905,23 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
const char* file_path = NULL;
for (int i = 0; i < tf_countof(k_map) && !after; i++)
{
after = _after(request->path, k_map[i][0]);
file_path = k_map[i][1];
is_core = is_core || (after && i == 0);
const char* next_after = _after(request->path, k_map[i][0]);
if (next_after)
{
after = next_after;
file_path = k_map[i][1];
is_core = after && i == 0;
}
}
if ((!after || !*after) && request->path[strlen(request->path) - 1] == '/')
{
after = "index.html";
if (!file_path)
{
file_path = "core/";
is_core = true;
}
}
if (!after || strstr(after, ".."))
@ -783,6 +959,38 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}
static void _httpd_endpoint_root_callback(const char* path, void* user_data)
{
tf_http_request_t* request = user_data;
const char* host = tf_http_request_get_header(request, "x-forwarded-host");
if (!host)
{
host = tf_http_request_get_header(request, "host");
}
char url[1024];
snprintf(url, sizeof(url), "%s%s%s", request->is_tls ? "https://" : "http://", host, path ? path : "/~core/apps/");
const char* headers[] = {
"Location",
url,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_http_request_unref(request);
}
static void _httpd_endpoint_root(tf_http_request_t* request)
{
const char* host = tf_http_request_get_header(request, "x-forwarded-host");
if (!host)
{
host = tf_http_request_get_header(request, "host");
}
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_http_request_ref(request);
tf_ssb_db_resolve_index_async(ssb, host, _httpd_endpoint_root_callback, request);
}
static void _httpd_endpoint_robots_txt(tf_http_request_t* request)
{
if (_httpd_redirect(request))
@ -895,13 +1103,17 @@ const char* _form_data_get(const char** form_data, const char* key)
typedef struct _login_request_t
{
tf_http_request_t* request;
const char* session_cookie;
JSValue jwt;
const char* name;
const char* error;
const char* settings;
const char* code_of_conduct;
bool have_administrator;
bool session_is_new;
char location_header[1024];
const char* set_cookie_header;
int pending;
} login_request_t;
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
@ -916,18 +1128,29 @@ static const char* _make_set_session_cookie_header(tf_http_request_t* request, c
return cookie;
}
static void _login_release(login_request_t* login)
{
int ref_count = --login->pending;
if (ref_count == 0)
{
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->set_cookie_header);
tf_free(login);
}
}
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (result >= 0)
{
const char* cookie = _make_set_session_cookie_header(request, login->session_cookie);
const char* headers[] = {
"Content-Type",
"text/html; charset=utf-8",
"Set-Cookie",
cookie ? cookie : "",
login->set_cookie_header ? login->set_cookie_header : "",
};
const char* replace_me = "$AUTH_DATA";
const char* auth = strstr(data, replace_me);
@ -961,7 +1184,6 @@ static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char
{
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
tf_free((void*)cookie);
}
else
{
@ -969,10 +1191,7 @@ static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->session_cookie);
tf_free(login);
_login_release(login);
}
static bool _string_property_equals(JSContext* context, JSValue object, const char* name, const char* value)
@ -985,12 +1204,7 @@ static bool _string_property_equals(JSContext* context, JSValue object, const ch
return equals;
}
static void _public_key_visit(const char* identity, void* user_data)
{
snprintf(user_data, k_id_base64_len, "%s", identity);
}
static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt)
{
if (!jwt)
{
@ -1031,10 +1245,8 @@ static JSValue _authenticate_jwt(JSContext* context, const char* jwt)
return JS_UNDEFINED;
}
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
char public_key_b64[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":auth", _public_key_visit, public_key_b64);
tf_ssb_whoami(ssb, public_key_b64, sizeof(public_key_b64));
const char* payload = jwt + dot[0] + 1;
size_t payload_length = dot[1] - dot[0] - 1;
@ -1093,45 +1305,16 @@ static bool _is_name_valid(const char* name)
return true;
}
static void _visit_auth_identity(const char* identity, void* user_data)
{
if (!*(char*)user_data)
{
snprintf((char*)user_data, k_id_base64_len, "%s", identity);
}
}
static bool _get_auth_private_key(tf_ssb_t* ssb, uint8_t* out_private_key)
{
char id[k_id_base64_len] = { 0 };
tf_ssb_db_identity_visit(ssb, ":auth", _visit_auth_identity, id);
if (*id)
{
return tf_ssb_db_identity_get_private_key(ssb, ":auth", id, out_private_key, crypto_sign_SECRETKEYBYTES);
}
else
{
return tf_ssb_db_identity_create(ssb, ":auth", out_private_key + crypto_sign_PUBLICKEYBYTES, out_private_key);
}
}
static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name)
{
if (!name || !*name)
{
return NULL;
}
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
if (!_get_auth_private_key(ssb, private_key))
{
return NULL;
}
uv_timespec64_t now = { 0 };
uv_clock_gettime(UV_CLOCK_REALTIME, &now);
JSContext* context = tf_ssb_get_context(ssb);
const char* header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
char header_base64[256];
sodium_bin2base64(header_base64, sizeof(header_base64), (uint8_t*)header_json, strlen(header_json), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
@ -1150,6 +1333,9 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
unsigned long long signature_length = 0;
char signature_base64[256] = { 0 };
uint8_t private_key[crypto_sign_SECRETKEYBYTES] = { 0 };
tf_ssb_get_private_key(ssb, private_key, sizeof(private_key));
if (crypto_sign_detached(signature, &signature_length, (const uint8_t*)payload_base64, strlen(payload_base64), private_key) == 0)
{
sodium_bin2base64(signature_base64, sizeof(signature_base64), signature, sizeof(signature), sodium_base64_VARIANT_URLSAFE_NO_PADDING);
@ -1157,6 +1343,7 @@ static const char* _make_session_jwt(tf_ssb_t* ssb, const char* name)
result = tf_malloc(size);
snprintf(result, size, "%s.%s.%s", header_base64, payload_base64, signature_base64);
}
sodium_memzero(private_key, sizeof(private_key));
JS_FreeCString(context, payload_string);
JS_FreeValue(context, payload_json);
@ -1172,30 +1359,104 @@ static bool _verify_password(const char* password, const char* hash)
return out_hash && strcmp(hash, out_hash) == 0;
}
static void _httpd_endpoint_login(tf_http_request_t* request)
static bool _make_administrator_if_first(tf_ssb_t* ssb, JSContext* context, const char* account_name_copy, bool may_become_first_admin)
{
tf_task_t* task = request->user_data;
JSContext* context = tf_task_get_context(task);
tf_ssb_t* ssb = tf_task_get_ssb(task);
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings && *settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
if (JS_IsUndefined(settings_value))
{
settings_value = JS_NewObject(context);
}
bool have_administrator = false;
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
for (int i = 0; i < (int)plen; i++)
{
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
{
int permission_length = tf_util_get_length(context, desc.value);
for (int i = 0; i < permission_length; i++)
{
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
const char* permission = JS_ToCString(context, entry);
if (permission && strcmp(permission, "administration") == 0)
{
have_administrator = true;
}
JS_FreeCString(context, permission);
JS_FreeValue(context, entry);
}
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
JS_FreeValue(context, desc.value);
}
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
if (!have_administrator && may_become_first_admin)
{
if (JS_IsUndefined(permissions))
{
permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "permissions", JS_DupValue(context, permissions));
}
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
if (JS_IsUndefined(user))
{
user = JS_NewArray(context);
JS_SetPropertyStr(context, permissions, account_name_copy, JS_DupValue(context, user));
}
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
JS_FreeValue(context, user);
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
return have_administrator;
}
static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
const char** form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0);
const char* account_name_copy = NULL;
JSValue jwt = _authenticate_jwt(context, session);
JSValue jwt = _authenticate_jwt(ssb, context, session);
if (_session_is_authenticated_as_user(context, jwt))
{
const char* return_url = _form_data_get(form_data, "return");
char url[1024];
if (!return_url)
if (return_url)
{
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
return_url = url;
snprintf(login->location_header, sizeof(login->location_header), "%s", return_url);
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
const char* headers[] = {
"Location",
return_url,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
goto done;
}
@ -1222,27 +1483,39 @@ static void _httpd_endpoint_login(tf_http_request_t* request)
if (form_register && strcmp(form_register, "1") == 0)
{
if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0 &&
tf_ssb_db_register_account(ssb, account_name, password))
bool registered = false;
if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{
tf_free((void*)send_session);
send_session = _make_session_jwt(ssb, account_name);
may_become_first_admin = true;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password);
tf_ssb_release_db_writer(ssb, db);
if (registered)
{
tf_free((void*)send_session);
send_session = _make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
}
else
if (!registered)
{
login_error = "Error registering account.";
}
}
else if (change && strcmp(change, "1") == 0)
{
if (have_account && _is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 && _verify_password(password, account_passwd) &&
tf_ssb_db_set_account_password(ssb, account_name, new_password))
bool set = false;
if (have_account && _is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 && _verify_password(password, account_passwd))
{
tf_free((void*)send_session);
send_session = _make_session_jwt(ssb, account_name);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
set = tf_ssb_db_set_account_password(tf_ssb_get_loop(ssb), db, context, account_name, new_password);
tf_ssb_release_db_writer(ssb, db);
if (set)
{
tf_free((void*)send_session);
send_session = _make_session_jwt(context, ssb, account_name);
}
}
else
if (!set)
{
login_error = "Error changing password.";
}
@ -1252,7 +1525,7 @@ static void _httpd_endpoint_login(tf_http_request_t* request)
if (have_account && *account_passwd && _verify_password(password, account_passwd))
{
tf_free((void*)send_session);
send_session = _make_session_jwt(ssb, account_name);
send_session = _make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
else
@ -1264,115 +1537,56 @@ static void _httpd_endpoint_login(tf_http_request_t* request)
else
{
tf_free((void*)send_session);
send_session = _make_session_jwt(ssb, "guest");
send_session = _make_session_jwt(context, ssb, "guest");
}
tf_free(post_form_data);
}
bool have_administrator = _make_administrator_if_first(ssb, context, account_name_copy, may_become_first_admin);
if (session_is_new && _form_data_get(form_data, "return") && !login_error)
{
const char* return_url = _form_data_get(form_data, "return");
char url[1024];
if (!return_url)
if (return_url)
{
snprintf(url, sizeof(url), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
return_url = url;
snprintf(login->location_header, sizeof(login->location_header), "%s", return_url);
}
const char* cookie = _make_set_session_cookie_header(request, send_session);
const char* headers[] = {
"Location",
return_url,
"Set-Cookie",
cookie ? cookie : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_free((void*)cookie);
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
login->set_cookie_header = _make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
}
else
{
login->name = account_name_copy;
login->error = login_error;
login->set_cookie_header = _make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
login->session_is_new = session_is_new;
login->have_administrator = have_administrator;
login->settings = tf_ssb_db_get_property(ssb, "core", "settings");
if (login->settings)
{
JSValue settings_value = JS_ParseJSON(context, login->settings, strlen(login->settings), NULL);
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
const char* result = tf_strdup(code_of_conduct);
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)login->settings);
login->settings = NULL;
login->code_of_conduct = result;
}
login->pending++;
tf_http_request_ref(request);
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
bool have_administrator = false;
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
for (int i = 0; i < (int)plen; i++)
{
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
{
int permission_length = tf_util_get_length(context, desc.value);
for (int i = 0; i < permission_length; i++)
{
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
const char* permission = JS_ToCString(context, entry);
if (permission && strcmp(permission, "administration") == 0)
{
have_administrator = true;
}
JS_FreeCString(context, permission);
JS_FreeValue(context, entry);
}
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
JS_FreeValue(context, desc.value);
}
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
if (!have_administrator && may_become_first_admin)
{
if (JS_IsUndefined(permissions))
{
permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "permissions", permissions);
}
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
if (JS_IsUndefined(user))
{
user = JS_NewArray(context);
JS_SetPropertyStr(context, permissions, account_name_copy, user);
}
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
JS_FreeValue(context, permissions);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t) {
.request = request,
.name = account_name_copy,
.jwt = jwt,
.error = login_error,
.session_cookie = send_session,
.session_is_new = session_is_new,
.code_of_conduct = tf_strdup(code_of_conduct),
.have_administrator = have_administrator,
};
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
tf_file_read(request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
jwt = JS_UNDEFINED;
account_name_copy = NULL;
}
@ -1381,6 +1595,44 @@ done:
tf_free(form_data);
tf_free((void*)account_name_copy);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (login->pending == 1)
{
if (*login->location_header)
{
const char* headers[] = {
"Location",
login->location_header,
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
}
tf_http_request_unref(request);
_login_release(login);
}
static void _httpd_endpoint_login(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t) {
.request = request,
};
login->pending++;
tf_ssb_run_work(ssb, _httpd_endpoint_login_work, _httpd_endpoint_login_after_work, login);
}
static void _httpd_endpoint_logout(tf_http_request_t* request)
@ -1429,12 +1681,15 @@ void tf_httpd_register(JSContext* context)
tf_http_set_trace(http, tf_task_get_trace(task));
JS_SetOpaque(httpd, http);
tf_http_add_handler(http, "/codemirror/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/lit/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/prettier/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/speedscope/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/static/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/.well-known/", _httpd_endpoint_static, NULL, task);
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);
tf_http_add_handler(http, "/prettier/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/speedscope/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/static/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/.well-known/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);
@ -1451,6 +1706,8 @@ void tf_httpd_register(JSContext* context)
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));
JS_SetPropertyStr(context, httpd, "auth_query", JS_NewCFunction(context, _httpd_auth_query, "auth_query", 1));
JS_SetPropertyStr(context, httpd, "mime_type_from_magic_bytes", JS_NewCFunction(context, _httpd_mime_type_from_magic_bytes, "mime_type_from_magic_bytes", 1));
JS_SetPropertyStr(context, httpd, "mime_type_from_extension", JS_NewCFunction(context, _httpd_mime_type_from_extension, "mime_type_from_extension", 1));
JS_SetPropertyStr(context, global, "httpd", httpd);
JS_FreeValue(context, global);
}

View File

@ -9,6 +9,7 @@
#include "tests.h"
#include "util.js.h"
#include "ares.h"
#include "backtrace.h"
#include "sqlite3.h"
#include "unzip.h"
@ -34,6 +35,10 @@
#include <unistd.h>
#endif
#if defined(__ANDROID__)
#include "jni.h"
#endif
#if !defined(_countof)
#define _countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#endif
@ -48,6 +53,7 @@ static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_export(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_verify(const char* file, int argc, char* argv[]);
static int _tf_command_usage(const char* file);
typedef struct _command_t
@ -62,6 +68,7 @@ const command_t k_commands[] = {
{ "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." },
{ "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." },
{ "verify", _tf_command_verify, "Verify a feed." },
{ "test", _tf_command_test, "Test SSB." },
};
@ -266,6 +273,59 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
tf_ssb_destroy(ssb);
return EXIT_SUCCESS;
}
static int _tf_command_verify(const char* file, int argc, char* argv[])
{
const char* identity = NULL;
const char* db_path = k_db_path_default;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "id", required_argument, NULL, 'u' },
{ "db-path", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "i:d:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'i':
identity = optarg;
break;
case 'd':
db_path = optarg;
break;
}
}
if (show_usage)
{
tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("options:\n");
tf_printf(" -i, --identity identity Identity to verify.\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -h, --help Show this usage information.\n");
return EXIT_FAILURE;
}
tf_printf("Verifying %s...\n", identity);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
bool verified = tf_ssb_db_verify(ssb, identity);
tf_ssb_destroy(ssb);
return verified ? EXIT_SUCCESS : EXIT_FAILURE;
}
#endif
typedef struct tf_run_args_t
@ -326,6 +386,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
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))
{
tf_task_run(task);
@ -547,14 +608,16 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
static int _tf_command_sandbox(const char* file, int argc, char* argv[])
{
bool show_usage = false;
int fd = STDIN_FILENO;
while (!show_usage)
{
static const struct option k_options[] = {
{ "fd", required_argument, NULL, 'f' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "h", k_options, NULL);
int c = getopt_long(argc, argv, "f:h", k_options, NULL);
if (c == -1)
{
break;
@ -566,6 +629,9 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
default:
show_usage = true;
break;
case 'f':
fd = atoi(optarg);
break;
}
}
@ -574,6 +640,7 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
tf_printf("\nUsage: %s sandbox [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_printf(" -f, --fd File descriptor with which to communicate with parent process.\n");
return EXIT_FAILURE;
}
@ -581,7 +648,7 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
prctl(PR_SET_PDEATHSIG, SIGHUP);
#endif
tf_task_t* task = tf_task_create();
tf_task_configure_from_fd(task, STDIN_FILENO);
tf_task_configure_from_fd(task, fd);
_shed_privileges();
/* The caller will trigger tf_task_activate with a message. */
tf_task_run(task);
@ -629,10 +696,6 @@ static void _startup(int argc, char* argv[])
}
}
#if defined(__ANDROID__)
setenv("UV_USE_IO_URING", "0", 1);
#endif
tf_mem_startup(tracking);
g_backtrace_state = backtrace_create_state(argv[0], 0, _backtrace_error, NULL);
@ -660,7 +723,7 @@ static void _startup(int argc, char* argv[])
{
if (
#if !defined(_WIN32)
signal(SIGSYS, _error_handler) == SIG_ERR ||
signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR ||
#endif
signal(SIGSEGV, _error_handler) == SIG_ERR)
{
@ -670,23 +733,143 @@ static void _startup(int argc, char* argv[])
}
#if defined(__ANDROID__)
static JNIEnv* s_jni_env;
static void _tf_service_start(int pipe_fd)
{
tf_printf("_tf_service_start\n");
jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity");
jmethodID start_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "start_sandbox", "(I)V");
(*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, start_sandbox, pipe_fd);
}
static void _tf_service_stop()
{
tf_printf("_tf_service_stop\n");
jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity");
jmethodID stop_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "stop_sandbox", "()V");
(*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, stop_sandbox);
}
static jint _tf_server_main(JNIEnv* env, jobject this_object, jstring files_dir, jstring apk_path, jstring out_port_file_path, jobject connectivity_manager)
{
s_jni_env = env;
tf_printf("This is tf_server_main main.\n");
_startup(0, (char*[]) { NULL });
tf_printf("That was startup.\n");
ares_library_init_android(connectivity_manager);
ares_library_init(0);
const char* files = (*env)->GetStringUTFChars(env, files_dir, NULL);
const char* apk = (*env)->GetStringUTFChars(env, apk_path, NULL);
const char* out_port_file = (*env)->GetStringUTFChars(env, out_port_file_path, NULL);
tf_printf("FILES = %s\n", files);
tf_printf("APK = %s\n", apk);
tf_printf("OUT_PORT = %s\n", out_port_file);
int result = uv_chdir(files);
if (result)
{
tf_printf("uv_chdir: %s\n", uv_strerror(result));
}
size_t port_file_arg_length = strlen(out_port_file) + strlen("out_http_port_file=") + 1;
char* port_file_arg = alloca(port_file_arg_length);
snprintf(port_file_arg, port_file_arg_length, "out_http_port_file=%s", out_port_file);
const char* args[] = {
"run",
"-z",
apk,
"-a",
port_file_arg,
"-p",
"0",
};
tf_task_set_android_service_callbacks(_tf_service_start, _tf_service_stop);
result = _tf_command_run(apk, _countof(args), (char**)args);
tf_task_set_android_service_callbacks(NULL, NULL);
(*env)->ReleaseStringUTFChars(env, files_dir, files);
(*env)->ReleaseStringUTFChars(env, apk_path, apk);
(*env)->ReleaseStringUTFChars(env, out_port_file_path, out_port_file);
ares_library_cleanup();
tf_mem_shutdown();
tf_printf("tf_server_main finished with %d.", result);
s_jni_env = NULL;
return result;
}
static jint _tf_sandbox_main(JNIEnv* env, jobject this_object, int pipe_fd)
{
s_jni_env = env;
tf_printf("This is tf_sandbox_main main (fd=%d).\n", pipe_fd);
_startup(0, (char*[]) { NULL });
tf_printf("That was startup.\n");
char fd[32] = { 0 };
snprintf(fd, sizeof(fd), "%d", pipe_fd);
const char* args[] = {
"sandbox",
"-f",
fd,
};
int result = _tf_command_sandbox(NULL, _countof(args), (char**)args);
tf_mem_shutdown();
tf_printf("tf_sandbox_main finished with %d.", result);
s_jni_env = NULL;
return result;
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
tf_printf("JNI_Onload called.\n");
JNIEnv* env;
if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK)
{
tf_printf("Failed to get JNI environment.\n");
return JNI_ERR;
}
tf_printf("Finding class.\n");
jclass c = (*env)->FindClass(env, "com/unprompted/tildefriends/TildeFriendsActivity");
if (!c)
{
tf_printf("Failed to find TildeFriendsActivity class.\n");
return JNI_ERR;
}
tf_printf("Registering method.\n");
static const JNINativeMethod methods[] = {
{ "tf_server_main", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/net/ConnectivityManager;)I", _tf_server_main },
{ "tf_sandbox_main", "(I)I", _tf_sandbox_main },
};
int result = (*env)->RegisterNatives(env, c, methods, (int)_countof(methods));
if (result != JNI_OK)
{
return result;
}
ares_library_init_jvm(vm);
tf_printf("Done.\n");
return JNI_VERSION_1_6;
}
int main(int argc, char* argv[])
{
_startup(argc, argv);
int result = -1;
if (argc > 1)
{
if (strcmp(argv[1], "run") == 0)
{
result = _tf_command_run(argv[0], argc - 1, argv + 1);
}
else if (strcmp(argv[1], "sandbox") == 0)
{
result = _tf_command_sandbox(argv[0], argc - 1, argv + 1);
}
}
tf_mem_shutdown();
return result;
tf_printf("Welcome to Tilde Friends. This is not the way to run on Android.\n");
return EXIT_FAILURE;
}
#elif TARGET_OS_IPHONE
void tf_run_thread_start(const char* zip_path)
@ -713,6 +896,7 @@ void tf_run_thread_start(const char* zip_path)
int main(int argc, char* argv[])
{
_startup(argc, argv);
ares_library_init(0);
int result = 0;
if (argc >= 2)
@ -733,6 +917,7 @@ int main(int argc, char* argv[])
result = _tf_command_run(argv[0], argc, argv);
}
done:
ares_library_cleanup();
tf_mem_shutdown();
return result;
}

652
src/ssb.c

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@ static void _tf_ssb_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t
}
break;
case k_tf_ssb_change_remove:
case k_tf_ssb_change_update:
break;
}
}
@ -75,32 +76,32 @@ static bool _tf_ssb_connections_get_next_connection(tf_ssb_connections_t* connec
typedef struct _tf_ssb_connections_get_next_t
{
uv_work_t work;
tf_ssb_connections_t* connections;
tf_ssb_t* ssb;
bool ready;
char host[256];
int port;
char key[k_id_base64_len];
} tf_ssb_connections_get_next_t;
static void _tf_ssb_connections_get_next_work(uv_work_t* work)
static void _tf_ssb_connections_get_next_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_connections_get_next_t* next = work->data;
tf_ssb_record_thread_busy(next->ssb, true);
tf_ssb_connections_get_next_t* next = user_data;
if (tf_ssb_is_shutting_down(ssb))
{
return;
}
next->ready = _tf_ssb_connections_get_next_connection(next->connections, next->host, sizeof(next->host), &next->port, next->key, sizeof(next->key));
tf_ssb_record_thread_busy(next->ssb, false);
}
static void _tf_ssb_connections_get_next_after_work(uv_work_t* work, int status)
static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
tf_ssb_connections_get_next_t* next = work->data;
tf_ssb_connections_get_next_t* next = user_data;
if (next->ready)
{
uint8_t key_bin[k_id_bin_len];
if (tf_ssb_id_str_to_bin(key_bin, next->key))
{
tf_ssb_connect(next->ssb, next->host, next->port, key_bin);
tf_ssb_connect(ssb, next->host, next->port, key_bin);
}
}
tf_free(next);
@ -114,20 +115,10 @@ static void _tf_ssb_connections_timer(uv_timer_t* timer)
if (count < (int)_countof(active))
{
tf_ssb_connections_get_next_t* next = tf_malloc(sizeof(tf_ssb_connections_get_next_t));
*next = (tf_ssb_connections_get_next_t)
{
.work =
{
.data = next,
},
.ssb = connections->ssb,
*next = (tf_ssb_connections_get_next_t) {
.connections = connections,
};
int result = uv_queue_work(tf_ssb_get_loop(connections->ssb), &next->work, _tf_ssb_connections_get_next_work, _tf_ssb_connections_get_next_after_work);
if (result)
{
_tf_ssb_connections_get_next_after_work(&next->work, result);
}
tf_ssb_run_work(connections->ssb, _tf_ssb_connections_get_next_work, _tf_ssb_connections_get_next_after_work, next);
}
}
@ -162,8 +153,6 @@ void tf_ssb_connections_destroy(tf_ssb_connections_t* connections)
typedef struct _tf_ssb_connections_update_t
{
uv_work_t work;
tf_ssb_t* ssb;
char host[256];
int port;
char key[k_id_base64_len];
@ -171,12 +160,15 @@ typedef struct _tf_ssb_connections_update_t
bool succeeded;
} tf_ssb_connections_update_t;
static void _tf_ssb_connections_update_work(uv_work_t* work)
static void _tf_ssb_connections_update_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_connections_update_t* update = work->data;
tf_ssb_record_thread_busy(update->ssb, true);
tf_ssb_connections_update_t* update = user_data;
if (tf_ssb_is_shutting_down(ssb))
{
return;
}
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_writer(update->ssb);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
if (update->attempted)
{
if (sqlite3_prepare(db, "UPDATE connections SET last_attempt = strftime('%s', 'now') WHERE host = ?1 AND port = ?2 AND key = ?3", -1, &statement, NULL) == SQLITE_OK)
@ -223,31 +215,23 @@ static void _tf_ssb_connections_update_work(uv_work_t* work)
sqlite3_finalize(statement);
}
}
tf_ssb_release_db_writer(update->ssb, db);
tf_ssb_record_thread_busy(update->ssb, false);
tf_ssb_release_db_writer(ssb, db);
}
static void _tf_ssb_connections_update_after_work(uv_work_t* work, int status)
static void _tf_ssb_connections_update_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
tf_ssb_connections_update_t* update = work->data;
tf_free(update);
tf_free(user_data);
}
static void _tf_ssb_connections_queue_update(tf_ssb_connections_t* connections, tf_ssb_connections_update_t* update)
{
update->work.data = update;
int result = uv_queue_work(tf_ssb_get_loop(connections->ssb), &update->work, _tf_ssb_connections_update_work, _tf_ssb_connections_update_after_work);
if (result)
{
_tf_ssb_connections_update_after_work(&update->work, result);
}
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)
{
tf_ssb_connections_update_t* update = tf_malloc(sizeof(tf_ssb_connections_update_t));
*update = (tf_ssb_connections_update_t) {
.ssb = connections->ssb,
.port = port,
};
snprintf(update->host, sizeof(update->host), "%s", host);
@ -259,7 +243,6 @@ void tf_ssb_connections_set_attempted(tf_ssb_connections_t* connections, const c
{
tf_ssb_connections_update_t* update = tf_malloc(sizeof(tf_ssb_connections_update_t));
*update = (tf_ssb_connections_update_t) {
.ssb = connections->ssb,
.port = port,
.attempted = true,
};
@ -272,7 +255,6 @@ void tf_ssb_connections_set_succeeded(tf_ssb_connections_t* connections, const c
{
tf_ssb_connections_update_t* update = tf_malloc(sizeof(tf_ssb_connections_update_t));
*update = (tf_ssb_connections_update_t) {
.ssb = connections->ssb,
.port = port,
.succeeded = true,
};

View File

@ -19,8 +19,7 @@
typedef struct _message_store_t message_store_t;
static void _tf_ssb_db_store_message_work_finish(message_store_t* store);
static void _tf_ssb_db_store_message_after_work(uv_work_t* work, int status);
static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void* user_data);
static void _tf_ssb_db_exec(sqlite3* db, const char* statement)
{
@ -134,6 +133,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_sequence_index ON messages (author, sequence)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs ("
" id TEXT PRIMARY KEY,"
@ -164,6 +164,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
" private_key TEXT UNIQUE"
")");
_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'");
bool populate_fts = false;
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_fts')"))
@ -281,7 +282,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
tf_ssb_release_db_writer(ssb, db);
}
static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int64_t sequence, const char* previous)
static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int64_t sequence, const char* previous, bool* out_id_mismatch)
{
bool exists = false;
if (sequence == 1)
@ -291,12 +292,13 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
else
{
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT COUNT(*) FROM messages WHERE author = ?1 AND sequence = ?2 AND id = ?3", -1, &statement, NULL) == SQLITE_OK)
if (sqlite3_prepare(db, "SELECT COUNT(*), id != ?3 AS is_mismatch FROM messages WHERE author = ?1 AND sequence = ?2", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, sequence - 1) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
exists = sqlite3_column_int(statement, 0) != 0;
*out_id_mismatch = sqlite3_column_int(statement, 1) != 0;
}
sqlite3_finalize(statement);
}
@ -309,8 +311,9 @@ static int64_t _tf_ssb_db_store_message_raw(tf_ssb_t* ssb, const char* id, const
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
int64_t last_row_id = -1;
bool id_mismatch = false;
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous))
if (_tf_ssb_db_previous_message_exists(db, author, sequence, previous, &id_mismatch))
{
const char* query = "INSERT INTO messages (id, previous, author, sequence, timestamp, content, hash, signature, flags) VALUES (?, ?, ?, ?, ?, jsonb(?), "
"?, ?, ?) ON CONFLICT DO NOTHING";
@ -345,9 +348,15 @@ static int64_t _tf_ssb_db_store_message_raw(tf_ssb_t* ssb, const char* id, const
tf_printf("%s: prepare failed: %s\n", __FUNCTION__, sqlite3_errmsg(db));
}
}
else
else if (id_mismatch)
{
tf_printf("%p: Previous message doesn't exist for author=%s sequence=%" PRId64 ".\n", db, author, sequence);
/*
** Only warn if we find a previous message with the wrong ID.
** If a feed is forked, we would otherwise warn on every
** message when trying to receive what we don't have, and
** that's not helping anybody.
*/
tf_printf("%p: Previous message doesn't exist for author=%s sequence=%" PRId64 " previous=%s.\n", db, author, sequence, previous);
}
tf_ssb_release_db_writer(ssb, db);
return last_row_id;
@ -400,8 +409,6 @@ static char* _tf_ssb_db_get_message_blob_wants(tf_ssb_t* ssb, int64_t rowid)
typedef struct _message_store_t
{
uv_work_t work;
tf_ssb_t* ssb;
char id[k_id_base64_len];
char signature[512];
int flags;
@ -421,21 +428,16 @@ typedef struct _message_store_t
message_store_t* next;
} message_store_t;
static void _tf_ssb_db_store_message_work(uv_work_t* work)
static void _tf_ssb_db_store_message_work(tf_ssb_t* ssb, void* user_data)
{
message_store_t* store = work->data;
tf_ssb_record_thread_busy(store->ssb, true);
tf_trace_t* trace = tf_ssb_get_trace(store->ssb);
tf_trace_begin(trace, "message_store_work");
int64_t last_row_id = _tf_ssb_db_store_message_raw(store->ssb, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp,
store->content, store->length, store->signature, store->flags);
message_store_t* store = user_data;
int64_t last_row_id = _tf_ssb_db_store_message_raw(
ssb, store->id, *store->previous ? store->previous : NULL, store->author, store->sequence, store->timestamp, store->content, store->length, store->signature, store->flags);
if (last_row_id != -1)
{
store->out_stored = true;
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(store->ssb, last_row_id);
store->out_blob_wants = _tf_ssb_db_get_message_blob_wants(ssb, last_row_id);
}
tf_trace_end(trace);
tf_ssb_record_thread_busy(store->ssb, false);
}
static void _wake_up_queue(tf_ssb_t* ssb, tf_ssb_store_queue_t* queue)
@ -452,38 +454,19 @@ static void _wake_up_queue(tf_ssb_t* ssb, tf_ssb_store_queue_t* queue)
}
next->next = NULL;
queue->running = true;
int r = uv_queue_work(tf_ssb_get_loop(ssb), &next->work, _tf_ssb_db_store_message_work, _tf_ssb_db_store_message_after_work);
if (r)
{
_tf_ssb_db_store_message_work_finish(next);
}
tf_ssb_run_work(ssb, _tf_ssb_db_store_message_work, _tf_ssb_db_store_message_after_work, next);
}
}
}
static void _tf_ssb_db_store_message_work_finish(message_store_t* store)
static void _tf_ssb_db_store_message_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
JSContext* context = tf_ssb_get_context(store->ssb);
if (store->callback)
{
store->callback(store->id, store->out_stored, store->user_data);
}
JS_FreeCString(context, store->content);
tf_ssb_store_queue_t* queue = tf_ssb_get_store_queue(store->ssb);
queue->running = false;
_wake_up_queue(store->ssb, queue);
tf_free(store);
}
static void _tf_ssb_db_store_message_after_work(uv_work_t* work, int status)
{
message_store_t* store = work->data;
tf_trace_t* trace = tf_ssb_get_trace(store->ssb);
tf_trace_begin(trace, "message_store_after_work");
message_store_t* store = user_data;
tf_trace_t* trace = tf_ssb_get_trace(ssb);
if (store->out_stored)
{
tf_trace_begin(trace, "notify_message_added");
JSContext* context = tf_ssb_get_context(store->ssb);
JSContext* context = tf_ssb_get_context(ssb);
JSValue formatted =
tf_ssb_format_message(context, store->previous, store->author, store->sequence, store->timestamp, "sha256", store->content, store->signature, store->flags);
JSValue message = JS_NewObject(context);
@ -492,7 +475,7 @@ static void _tf_ssb_db_store_message_after_work(uv_work_t* work, int status)
char timestamp_string[256];
snprintf(timestamp_string, sizeof(timestamp_string), "%f", store->timestamp);
JS_SetPropertyStr(context, message, "timestamp", JS_NewString(context, timestamp_string));
tf_ssb_notify_message_added(store->ssb, store->id, message);
tf_ssb_notify_message_added(ssb, store->id, message);
JS_FreeValue(context, message);
tf_trace_end(trace);
}
@ -501,13 +484,22 @@ static void _tf_ssb_db_store_message_after_work(uv_work_t* work, int status)
tf_trace_begin(trace, "notify_blob_wants_added");
for (char* p = store->out_blob_wants; *p; p = p + strlen(p))
{
tf_ssb_notify_blob_want_added(store->ssb, p);
tf_ssb_notify_blob_want_added(ssb, p);
}
tf_free(store->out_blob_wants);
tf_trace_end(trace);
}
_tf_ssb_db_store_message_work_finish(store);
tf_trace_end(trace);
JSContext* context = tf_ssb_get_context(ssb);
if (store->callback)
{
store->callback(store->id, store->out_stored, store->user_data);
}
JS_FreeCString(context, store->content);
tf_ssb_store_queue_t* queue = tf_ssb_get_store_queue(ssb);
queue->running = false;
_wake_up_queue(ssb, queue);
tf_free(store);
}
void tf_ssb_db_store_message(
@ -539,13 +531,7 @@ void tf_ssb_db_store_message(
JS_FreeValue(context, contentval);
message_store_t* store = tf_malloc(sizeof(message_store_t));
*store = (message_store_t)
{
.work =
{
.data = store,
},
.ssb = ssb,
*store = (message_store_t) {
.sequence = sequence,
.timestamp = timestamp,
.content = contentstr,
@ -658,10 +644,46 @@ bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_
return result;
}
typedef struct _blob_get_async_t
{
tf_ssb_t* ssb;
char id[k_blob_id_len];
tf_ssb_db_blob_get_callback_t* callback;
void* user_data;
bool out_found;
uint8_t* out_data;
size_t out_size;
} blob_get_async_t;
static void _tf_ssb_db_blob_get_async_work(tf_ssb_t* ssb, void* user_data)
{
blob_get_async_t* async = user_data;
async->out_found = tf_ssb_db_blob_get(ssb, async->id, &async->out_data, &async->out_size);
}
static void _tf_ssb_db_blob_get_async_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
blob_get_async_t* async = user_data;
async->callback(async->out_found, async->out_data, async->out_size, async->user_data);
tf_free(async->out_data);
tf_free(async);
}
void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_callback_t* callback, void* user_data)
{
blob_get_async_t* async = tf_malloc(sizeof(blob_get_async_t));
*async = (blob_get_async_t) {
.ssb = ssb,
.callback = callback,
.user_data = user_data,
};
snprintf(async->id, sizeof(async->id), "%s", id);
tf_ssb_run_work(ssb, _tf_ssb_db_blob_get_async_work, _tf_ssb_db_blob_get_async_after_work, async);
}
typedef struct _blob_store_work_t
{
uv_work_t work;
tf_ssb_t* ssb;
const uint8_t* blob;
size_t size;
char id[k_blob_id_len];
@ -670,25 +692,21 @@ typedef struct _blob_store_work_t
void* user_data;
} blob_store_work_t;
static void _tf_ssb_db_blob_store_work(uv_work_t* work)
static void _tf_ssb_db_blob_store_work(tf_ssb_t* ssb, void* user_data)
{
blob_store_work_t* blob_work = work->data;
tf_ssb_record_thread_busy(blob_work->ssb, true);
tf_trace_t* trace = tf_ssb_get_trace(blob_work->ssb);
tf_trace_begin(trace, "blob_store_work");
tf_ssb_db_blob_store(blob_work->ssb, blob_work->blob, blob_work->size, blob_work->id, sizeof(blob_work->id), &blob_work->is_new);
tf_trace_end(trace);
tf_ssb_record_thread_busy(blob_work->ssb, false);
blob_store_work_t* blob_work = user_data;
if (!tf_ssb_is_shutting_down(ssb))
{
tf_ssb_db_blob_store(ssb, blob_work->blob, blob_work->size, blob_work->id, sizeof(blob_work->id), &blob_work->is_new);
}
}
static void _tf_ssb_db_blob_store_after_work(uv_work_t* work, int status)
static void _tf_ssb_db_blob_store_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
blob_store_work_t* blob_work = work->data;
tf_trace_t* trace = tf_ssb_get_trace(blob_work->ssb);
tf_trace_begin(trace, "blob_store_after_work");
blob_store_work_t* blob_work = user_data;
if (status == 0 && *blob_work->id)
{
tf_ssb_notify_blob_stored(blob_work->ssb, blob_work->id);
tf_ssb_notify_blob_stored(ssb, blob_work->id);
}
if (status != 0)
{
@ -698,35 +716,19 @@ static void _tf_ssb_db_blob_store_after_work(uv_work_t* work, int status)
{
blob_work->callback(status == 0 ? blob_work->id : NULL, blob_work->is_new, blob_work->user_data);
}
tf_trace_end(trace);
tf_free(blob_work);
}
void tf_ssb_db_blob_store_async(tf_ssb_t* ssb, const uint8_t* blob, size_t size, tf_ssb_db_blob_store_callback_t* callback, void* user_data)
{
blob_store_work_t* work = tf_malloc(sizeof(blob_store_work_t));
*work = (blob_store_work_t)
{
.work =
{
.data = work,
},
.ssb = ssb,
*work = (blob_store_work_t) {
.blob = blob,
.size = size,
.callback = callback,
.user_data = user_data,
};
int r = uv_queue_work(tf_ssb_get_loop(ssb), &work->work, _tf_ssb_db_blob_store_work, _tf_ssb_db_blob_store_after_work);
if (r)
{
tf_printf("tf_ssb_db_blob_store_async -> uv_queue_work failed immediately: %s\n", uv_strerror(r));
if (callback)
{
callback(NULL, false, user_data);
}
tf_free(work);
}
tf_ssb_run_work(ssb, _tf_ssb_db_blob_store_work, _tf_ssb_db_blob_store_after_work, work);
}
bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char* out_id, size_t out_id_size, bool* out_new)
@ -784,12 +786,12 @@ bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char*
return result;
}
bool tf_ssb_db_get_message_by_author_and_sequence(
tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, double* out_timestamp, char** out_content)
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
size_t out_previous_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, size_t out_signature_size, int* out_flags)
{
bool found = false;
sqlite3_stmt* statement;
const char* query = "SELECT id, timestamp, json(content) FROM messages WHERE author = ?1 AND sequence = ?2";
const char* query = "SELECT id, previous, timestamp, json(content), hash, signature, flags FROM messages WHERE author = ?1 AND sequence = ?2";
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK)
{
@ -797,15 +799,41 @@ bool tf_ssb_db_get_message_by_author_and_sequence(
{
if (out_message_id)
{
strncpy(out_message_id, (const char*)sqlite3_column_text(statement, 0), out_message_id_size - 1);
snprintf(out_message_id, out_message_id_size, "%s", (const char*)sqlite3_column_text(statement, 0));
}
if (out_previous)
{
if (sqlite3_column_type(statement, 1) == SQLITE_NULL)
{
if (out_previous_size)
{
*out_previous = '\0';
}
}
else
{
snprintf(out_previous, out_previous_size, "%s", (const char*)sqlite3_column_text(statement, 1));
}
}
if (out_timestamp)
{
*out_timestamp = sqlite3_column_double(statement, 1);
*out_timestamp = sqlite3_column_double(statement, 2);
}
if (out_content)
{
*out_content = tf_strdup((const char*)sqlite3_column_text(statement, 2));
*out_content = tf_strdup((const char*)sqlite3_column_text(statement, 3));
}
if (out_hash)
{
snprintf(out_hash, out_hash_size, "%s", (const char*)sqlite3_column_text(statement, 4));
}
if (out_signature)
{
snprintf(out_signature, out_signature_size, "%s", (const char*)sqlite3_column_text(statement, 5));
}
if (out_flags)
{
*out_flags = sqlite3_column_int(statement, 6);
}
found = true;
}
@ -1068,6 +1096,8 @@ bool tf_ssb_db_identity_create(tf_ssb_t* ssb, const char* user, uint8_t* out_pub
if (out_private_key)
{
tf_ssb_id_str_to_bin(out_private_key, private);
/* HACK: tf_ssb_id_str_to_bin only produces 32 bytes even though the full private key is 32 + 32. */
tf_ssb_id_str_to_bin(out_private_key + crypto_sign_PUBLICKEYBYTES, public);
}
return true;
}
@ -1615,14 +1645,13 @@ bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char*
return result;
}
bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password)
bool tf_ssb_db_set_account_password(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password)
{
JSContext* context = tf_ssb_get_context(ssb);
bool result = false;
static const int k_salt_length = 12;
char buffer[16];
size_t bytes = uv_random(tf_ssb_get_loop(ssb), &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
size_t bytes = uv_random(loop, &(uv_random_t) { 0 }, buffer, sizeof(buffer), 0, NULL) == 0 ? sizeof(buffer) : 0;
char output[7 + 22 + 1];
char* salt = crypt_gensalt_rn("$2b$", k_salt_length, buffer, bytes, output, sizeof(output));
char hash_output[7 + 22 + 31 + 1];
@ -1634,7 +1663,6 @@ bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char*
size_t user_length = 0;
const char* user_string = JS_ToCStringLen(context, &user_length, user_json);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'user:' || ?, ?)", -1, &statement, NULL) == SQLITE_OK)
{
@ -1644,7 +1672,6 @@ bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char*
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
JS_FreeCString(context, user_string);
JS_FreeValue(context, user_json);
@ -1652,46 +1679,70 @@ bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char*
return result;
}
bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password)
static bool _tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool default_value)
{
bool result = default_value;
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 && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
result = sqlite3_column_int(statement, 0) != 0;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password)
{
bool result = false;
JSContext* context = tf_ssb_get_context(ssb);
JSValue users_array = JS_UNDEFINED;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
bool registration_allowed = _tf_ssb_db_get_global_setting_bool(db, "account_registration", true);
if (registration_allowed)
{
if (sqlite3_step(statement) == SQLITE_ROW)
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = 'auth' AND key = 'users'", -1, &statement, NULL) == SQLITE_OK)
{
users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
if (sqlite3_step(statement) == SQLITE_ROW)
{
users_array = JS_ParseJSON(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0), NULL);
}
sqlite3_finalize(statement);
}
sqlite3_finalize(statement);
}
if (JS_IsUndefined(users_array))
{
users_array = JS_NewArray(context);
}
int length = tf_util_get_length(context, users_array);
JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));
JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
JS_FreeValue(context, users_array);
size_t value_length = 0;
const char* value = JS_ToCStringLen(context, &value_length, json);
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
if (JS_IsUndefined(users_array))
{
result = sqlite3_step(statement) == SQLITE_DONE;
users_array = JS_NewArray(context);
}
sqlite3_finalize(statement);
}
JS_FreeCString(context, value);
JS_FreeValue(context, json);
tf_ssb_release_db_writer(ssb, db);
int length = tf_util_get_length(context, users_array);
JS_SetPropertyUint32(context, users_array, length, JS_NewString(context, name));
result = result && tf_ssb_db_set_account_password(ssb, name, password);
JSValue json = JS_JSONStringify(context, users_array, JS_NULL, JS_NULL);
JS_FreeValue(context, users_array);
size_t value_length = 0;
const char* value = JS_ToCStringLen(context, &value_length, json);
if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ('auth', 'users', ?)", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, value, value_length, NULL) == SQLITE_OK)
{
tf_printf("added user to properties\n");
result = sqlite3_step(statement) == SQLITE_DONE;
}
sqlite3_finalize(statement);
}
JS_FreeCString(context, value);
JS_FreeValue(context, json);
}
result = result && tf_ssb_db_set_account_password(loop, db, context, name, password);
return result;
}
@ -1735,3 +1786,184 @@ bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, cons
tf_ssb_release_db_writer(ssb, db);
return result;
}
bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size)
{
sqlite3_stmt* statement = NULL;
bool found = false;
if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = 'id:' || ? || ':' || ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, package_owner, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, package_name, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
snprintf(out_identity, out_identity_size, "%s", (const char*)sqlite3_column_text(statement, 0));
found = true;
}
sqlite3_finalize(statement);
}
return found;
}
typedef struct _resolve_index_t
{
const char* host;
const char* path;
void (*callback)(const char* path, void* user_data);
void* user_data;
} resolve_index_t;
static void _tf_ssb_db_resolve_index_work(tf_ssb_t* ssb, void* user_data)
{
resolve_index_t* request = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.index_map') FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
const char* index_map = (const char*)sqlite3_column_text(statement, 0);
const char* start = index_map;
while (start)
{
const char* end = strchr(start, '\n');
const char* equals = strchr(start, '=');
if (equals && strncasecmp(request->host, start, equals - start) == 0)
{
size_t value_length = end && equals < end ? (size_t)(end - (equals + 1)) : strlen(equals + 1);
char* path = tf_malloc(value_length + 1);
memcpy(path, equals + 1, value_length);
path[value_length] = '\0';
request->path = path;
break;
}
start = end ? end + 1 : NULL;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
if (!request->path)
{
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.index') FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
request->path = tf_strdup((const char*)sqlite3_column_text(statement, 0));
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
}
tf_ssb_release_db_reader(ssb, db);
}
static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
resolve_index_t* request = user_data;
request->callback(request->path, request->user_data);
tf_free((void*)request->host);
tf_free((void*)request->path);
tf_free(request);
}
void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callback)(const char* path, void* user_data), void* user_data)
{
resolve_index_t* request = tf_malloc(sizeof(resolve_index_t));
*request = (resolve_index_t) {
.host = tf_strdup(host),
.callback = callback,
.user_data = user_data,
};
tf_ssb_run_work(ssb, _tf_ssb_db_resolve_index_work, _tf_ssb_db_resolve_index_after_work, request);
}
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
{
JSContext* context = tf_ssb_get_context(ssb);
bool verified = true;
int64_t sequence = -1;
if (tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0))
{
for (int64_t i = 1; i <= sequence; i++)
{
char message_id[k_id_base64_len];
char previous[256];
double timestamp;
char* content = NULL;
char hash[32];
char signature[256];
int flags = 0;
if (tf_ssb_db_get_message_by_author_and_sequence(
ssb, id, i, message_id, sizeof(message_id), previous, sizeof(previous), &timestamp, &content, hash, sizeof(hash), signature, sizeof(signature), &flags))
{
JSValue message = tf_ssb_format_message(context, previous, id, i, timestamp, hash, content, signature, flags);
char calculated_id[k_id_base64_len];
char extracted_signature[256];
int calculated_flags = 0;
if (!tf_ssb_verify_and_strip_signature(context, message, calculated_id, sizeof(calculated_id), extracted_signature, sizeof(extracted_signature), &calculated_flags))
{
tf_printf("author=%s sequence=%" PRId64 " verify failed.\n", id, i);
verified = false;
}
if (calculated_flags != flags)
{
tf_printf("author=%s sequence=%" PRId64 " flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
verified = false;
}
if (strcmp(message_id, calculated_id))
{
tf_printf("author=%s sequence=%" PRId64 " id mismatch %s => %s.\n", id, i, message_id, calculated_id);
verified = false;
}
JS_FreeValue(context, message);
tf_free(content);
if (!verified)
{
break;
}
}
else
{
tf_printf("Unable to find message with sequence=%" PRId64 " for author=%s.", i, id);
verified = false;
break;
}
}
}
else
{
tf_printf("Unable to get latest message for author '%s'.\n", id);
verified = false;
}
return verified;
}
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission)
{
bool has_permission = false;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db,
"SELECT COUNT(*) FROM properties, json_each(properties.value -> 'permissions' -> ?) AS permission WHERE properties.id = 'core' AND properties.key = 'settings' AND "
"permission.value = ?",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, permission, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW)
{
has_permission = sqlite3_column_int64(statement, 0) > 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return has_permission;
}

View File

@ -55,6 +55,24 @@ bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id);
*/
bool tf_ssb_db_blob_get(tf_ssb_t* ssb, const char* id, uint8_t** out_blob, size_t* out_size);
/**
** A function called when a blob is retrieved from the database.
** @param found Whether the blob was found.
** @param data The blob data if found.
** @param size The size of the blob data if found, in bytes.
** @param user_data The user data.
*/
typedef void(tf_ssb_db_blob_get_callback_t)(bool found, const uint8_t* data, size_t size, void* user_data);
/**
** Retrieve a blob from the database asynchronously.
** @param ssb The SSB instance.
** @param id The blob identifier.
** @param callback Callback called with the result.
** @param user_data The user data.
*/
void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_callback_t* callback, void* user_data);
/**
** A function called when a message is stored in the database.
** @param id The message identifier.
@ -122,12 +140,19 @@ JSValue tf_ssb_db_get_message_by_id(tf_ssb_t* ssb, const char* id, bool is_keys)
** @param sequence The message sequence number.
** @param[out] out_message_id Populated with the message identifier.
** @param out_message_id_size The size of the out_message_id buffer.
** @param[out] out_previous Populated with the previous message identifier.
** @param out_previous_size The size of the out_previous buffer.
** @param[out] out_timestamp Populated with the timestamp.
** @param[out] out_content Populated with the message content. Free with tf_free().
** @param[out] out_hash Populated with the message hash format.
** @param out_hash_size The size of the out_hash buffer.
** @param[out] out_signature Populated with the message signature.
** @param out_signature_size The size of the out_signature buffer.
** @param[out] out_flags Populated with flags describing the format of the message.
** @return True if the message was found and retrieved.
*/
bool tf_ssb_db_get_message_by_author_and_sequence(
tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, double* out_timestamp, char** out_content);
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
size_t out_previous_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, size_t out_signature_size, int* out_flags);
/**
** Get information about the last message from an author.
@ -195,6 +220,18 @@ bool tf_ssb_db_identity_delete(tf_ssb_t* ssb, const char* user, const char* publ
*/
bool tf_ssb_db_identity_add(tf_ssb_t* ssb, const char* user, const char* public_key, const char* private_key);
/**
** Get the active identity for a user for a given package.
** @param db An sqlite3 database.
** @param user The username.
** @param package_owner The username of the package owner.
** @param package_name The name of the package.
** @param[out] out_identity Populated with the identity.
** @param out_identity_size The size of the out_identity buffer.
** @return true If the identity was retrieved.
*/
bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size);
/**
** Call a function for each identity owned by a user.
** @param ssb The SSB instance.
@ -323,21 +360,25 @@ bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char*
/**
** Insert or update a user's hashed password in the database.
** @param ssb The SSB instance.
** @param loop The event loop.
** @param db A DB writer.
** @param context A JS context.
** @param name The username.
** @param password The raw password.
** @return true if the hash of the password was successfully stored.
*/
bool tf_ssb_db_set_account_password(tf_ssb_t* ssb, const char* name, const char* password);
bool tf_ssb_db_set_account_password(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password);
/**
** Add a user account to the database.
** @param ssb The SSB instance.
** @param loop The event loop.
** @param db A DB writer.
** @param context A JS context.
** @param name The username to add.
** @param password The user's raw password.
** @return true If the user was added successfully.
*/
bool tf_ssb_db_register_account(tf_ssb_t* ssb, const char* name, const char* password);
bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password);
/**
** Get an entry from the properties table.
@ -358,6 +399,32 @@ const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* ke
*/
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
/**
** Resolve a hostname to its index path by global settings.
** @param ssb The SSB instance.
** @param host The hostname.
** @param callback The callback.
** @param user_data The callback user data.
*/
void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callback)(const char* path, void* user_data), void* user_data);
/**
** Verify an author's feed.
** @param ssb The SSB instance.
** @param id The author'd identity.
** @return true If the feed verified successfully.
*/
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
/**
** Check if a user has a specific permission.
** @param ssb The SSB instance.
** @param id The user ID.
** @param permission The name of the permission.
** @return true If the user has the requested permission.
*/
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission);
/**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer.

125
src/ssb.h
View File

@ -28,6 +28,8 @@ enum
k_ssb_rpc_flag_new_request = 0x10,
k_ssb_blob_bytes_max = 5 * 1024 * 1024,
k_ssb_peer_exchange_expires_seconds = 60 * 60,
};
/**
@ -38,8 +40,19 @@ typedef enum _tf_ssb_change_t
k_tf_ssb_change_create,
k_tf_ssb_change_connect,
k_tf_ssb_change_remove,
k_tf_ssb_change_update,
} tf_ssb_change_t;
/**
** The origin of a broadcast entry.
*/
typedef enum _tf_ssb_broadcast_origin_t
{
k_tf_ssb_broadcast_origin_discovery,
k_tf_ssb_broadcast_origin_room,
k_tf_ssb_broadcast_origin_peer_exchange,
} tf_ssb_broadcast_origin_t;
/**
** Flags describing the structure of a message.
*/
@ -143,6 +156,13 @@ tf_ssb_t* tf_ssb_create(uv_loop_t* loop, JSContext* context, const char* db_path
*/
void tf_ssb_destroy(tf_ssb_t* ssb);
/**
** Checking if the SSB instance is in the process of shutting down.
** @param ssb The SSB instance.
** @return true If the SSB instance is shutting down.
*/
bool tf_ssb_is_shutting_down(tf_ssb_t* ssb);
/**
** Start optional periodic work.
** @param ssb The SSB instance.
@ -270,9 +290,11 @@ void tf_ssb_run(tf_ssb_t* ssb);
** @param author The author's public key.
** @param private_key The author's private key.
** @param message The message to sign.
** @param previous_id The ID of the previous message in the feed. Optional.
** @param previous_sequence The sequence number of the previous message in the feed. Optional.
** @return The signed message.
*/
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message);
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int64_t previous_sequence);
/**
** Get the server's identity.
@ -289,8 +311,18 @@ bool tf_ssb_whoami(tf_ssb_t* ssb, char* out_id, size_t out_id_size);
** @param callback The callback.
** @param user_data User data for the callback.
*/
void tf_ssb_visit_broadcasts(
tf_ssb_t* ssb, void (*callback)(const char* host, const struct sockaddr_in* addr, tf_ssb_connection_t* tunnel, const uint8_t* pub, void* user_data), void* user_data);
void tf_ssb_visit_broadcasts(tf_ssb_t* ssb,
void (*callback)(const char* host, const struct sockaddr_in* addr, tf_ssb_broadcast_origin_t origin, tf_ssb_connection_t* tunnel, const uint8_t* pub, void* user_data),
void* user_data);
/**
** Add a broadcast entry.
** @param ssb The SSB instance.
** @param connection The connection string to add.
** @param origin The origin of the broadcast entry.
** @param expires_seconds How long the broadcast entry should last.
*/
void tf_ssb_add_broadcast(tf_ssb_t* ssb, const char* connection, tf_ssb_broadcast_origin_t origin, int64_t expires_seconds);
/**
** Get the identities of all active connections.
@ -332,6 +364,13 @@ void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address);
*/
int tf_ssb_server_open(tf_ssb_t* ssb, int port);
/**
** Determine the port that a server is listening on.
** @param ssb The SSB instance.
** @return The port number, or 0 if not bound.
*/
int tf_ssb_server_get_port(tf_ssb_t* ssb);
/**
** Stop listening for SHS connections.
** @param ssb The SSB instance.
@ -658,27 +697,29 @@ void tf_ssb_remove_rpc_callback(tf_ssb_t* ssb, const char** name, tf_ssb_rpc_cal
** @param connection The connection on which to send the message.
** @param flags The message flags.
** @param request_number The request number.
** @param new_request_name The name of the request if it is new.
** @param message The message payload.
** @param size The size of the message.
** @param callback A callback to call if a response is received.
** @param cleanup A callback to call if the callback is removed.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const uint8_t* message, size_t size, tf_ssb_rpc_callback_t* callback,
tf_ssb_callback_cleanup_t* cleanup, void* user_data);
void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, const uint8_t* message, size_t size,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
** Send a JSON MUXRPC message.
** @param connection The connection on which to send the message.
** @param flags The message flags.
** @param request_number The request number.
** @param new_request_name The name of the request if it is new.
** @param message The JS message payload.
** @param callback A callback to call if a response is received.
** @param cleanup A callback to call if the callback is removed.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_connection_rpc_send_json(
tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue message, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
void tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, JSValue message,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
** Send a MUXRPC error message.
@ -703,13 +744,14 @@ void tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* co
** request number.
** @param connection The connection on which to register the callback.
** @param request_number The request number.
** @param name The name of the RPC request.
** @param callback The callback.
** @param cleanup The function to call when the callback is removed.
** @param user_data User data to pass to the callback.
** @param dependent_connection A connection, which, if removed, invalidates this request.
*/
void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t request_number, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data,
tf_ssb_connection_t* dependent_connection);
void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t request_number, const char* name, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup,
void* user_data, tf_ssb_connection_t* dependent_connection);
/**
** Remove a callback registered to be called when a message is received for the
@ -719,6 +761,13 @@ void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t requ
*/
void tf_ssb_connection_remove_request(tf_ssb_connection_t* connection, int32_t request_number);
/**
** Get a debug representation of active requests.
** @param connection The connection.
** @return The active requests as a JS object.
*/
JSValue tf_ssb_connection_requests_to_object(tf_ssb_connection_t* connection);
/**
** A function scheduled to be run later.
** @param connection The owning connection.
@ -727,7 +776,7 @@ void tf_ssb_connection_remove_request(tf_ssb_connection_t* connection, int32_t r
typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, void* user_data);
/**
** Schedule work to be run when the server is next idle.
** Schedule work to be run when the connection is next idle.
** @param connection The owning connection.
** @param callback The callback to call.
** @param user_data User data to pass to the callback.
@ -744,6 +793,16 @@ void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_sch
void tf_ssb_connection_run_work(tf_ssb_connection_t* connection, void (*work_callback)(tf_ssb_connection_t* connection, void* user_data),
void (*after_work_callback)(tf_ssb_connection_t* connection, int result, void* user_data), void* user_data);
/**
** Schedule work to run on a worker thread.
** @param ssb The owning SSB instance.
** @param work_callback The callback to run on a thread.
** @param after_work_callback The callback to run on the main thread when the work is complete.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_run_work(
tf_ssb_t* ssb, void (*work_callback)(tf_ssb_t* ssb, void* user_data), void (*after_work_callback)(tf_ssb_t* ssb, int result, void* user_data), void* user_data);
/**
** Register for new messages on a connection.
** @param connection The SHS connection.
@ -934,6 +993,34 @@ bool tf_ssb_is_room(tf_ssb_t* ssb);
*/
void tf_ssb_set_is_room(tf_ssb_t* ssb, bool is_room);
/**
** Get whether the running server supports replication of messages and blobs.
** @param ssb The SSB instance.
** @return True if the server is a replicator.
*/
bool tf_ssb_is_replicator(tf_ssb_t* ssb);
/**
** Set whether the running server supports replication of messages and blobs.
** @param ssb The SSB instance.
** @param is_replicator Whether to support replication.
*/
void tf_ssb_set_is_replicator(tf_ssb_t* ssb, bool is_replicator);
/**
** Get whether the running server participates in peer exchange.
** @param ssb The SSB instance.
** @return True if the server participates in peer exchange.
*/
bool tf_ssb_is_peer_exchange(tf_ssb_t* ssb);
/**
** Set whether the running server participates in peer exchange.
** @param ssb The SSB instance.
** @param is_peer_exchange Whether to participate in peer exchange.
*/
void tf_ssb_set_is_peer_exchange(tf_ssb_t* ssb, bool is_peer_exchange);
/**
** Get the name of the room hosted by the running server.
** @param ssb The SSB instance.
@ -968,4 +1055,22 @@ void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t
*/
bool tf_ssb_hmacsha256_verify(const char* public_key, const void* payload, size_t payload_length, const char* signature, bool signature_is_urlb64);
/**
** Adjust read backpressure. If it gets too high, TCP receive will be paused
** until it lowers.
** @param connection The connection on which to affect backpressure.
** @param delta The change in backpressure. Higher will eventually pause
** receive. Lower will resume it.
*/
void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection, int delta);
/**
** Adjust write count. Work scheduled by tf_ssb_connection_schedule_idle will
** only start when this reaches zero.
** @param connection The connection on which to affect backpressure.
** @param delta The change in write count. Higher will pause processing
** scheduled idle work queue. Lower will resume it.
*/
void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta);
/** @} */

View File

@ -231,28 +231,39 @@ static void _tf_ssb_import_app_json(tf_ssb_t* ssb, uv_loop_t* loop, JSContext* c
void tf_ssb_import(tf_ssb_t* ssb, const char* user, const char* path)
{
uv_fs_t req = { 0 };
int r = uv_fs_scandir(tf_ssb_get_loop(ssb), &req, path, 0, NULL);
if (r >= 0)
if (!path)
{
uv_dirent_t ent;
while (uv_fs_scandir_next(&req, &ent) == 0)
{
size_t len = strlen(path) + strlen(ent.name) + 2;
char* full_path = tf_malloc(len);
snprintf(full_path, len, "%s/%s", path, ent.name);
if (strlen(ent.name) > strlen(".json") && strcasecmp(ent.name + strlen(ent.name) - strlen(".json"), ".json") == 0)
{
_tf_ssb_import_app_json(ssb, tf_ssb_get_loop(ssb), tf_ssb_get_context(ssb), user, full_path);
}
tf_free(full_path);
}
return;
}
else if (strlen(path) > strlen(".json") && strcasecmp(path + strlen(path) - strlen(".json"), ".json") == 0)
{
_tf_ssb_import_app_json(ssb, tf_ssb_get_loop(ssb), tf_ssb_get_context(ssb), user, path);
}
else
{
tf_printf("Failed to scan directory %s: %s.", path, uv_strerror(r));
uv_fs_t req = { 0 };
int r = uv_fs_scandir(tf_ssb_get_loop(ssb), &req, path, 0, NULL);
if (r >= 0)
{
uv_dirent_t ent;
while (uv_fs_scandir_next(&req, &ent) == 0)
{
size_t len = strlen(path) + strlen(ent.name) + 2;
char* full_path = tf_malloc(len);
snprintf(full_path, len, "%s/%s", path, ent.name);
if (strlen(ent.name) > strlen(".json") && strcasecmp(ent.name + strlen(ent.name) - strlen(".json"), ".json") == 0)
{
_tf_ssb_import_app_json(ssb, tf_ssb_get_loop(ssb), tf_ssb_get_context(ssb), user, full_path);
}
tf_free(full_path);
}
}
else
{
tf_printf("Failed to scan directory %s: %s.", path, uv_strerror(r));
}
uv_fs_req_cleanup(&req);
}
uv_fs_req_cleanup(&req);
}
static char* _tf_ssb_import_read_current_file_from_zip(unzFile zip, size_t* size)

File diff suppressed because it is too large Load Diff

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