Compare commits

...

232 Commits

Author SHA1 Message Date
af13bfc920 Let's release 0.0.11.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4478 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-27 22:26:19 +00:00
e24fd92f85 OpenSSL 1.1.1w for Android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4477 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-27 22:23:52 +00:00
7e27cefe6a This made 32-bit happier.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4476 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-25 16:43:56 +00:00
450cf6424e Oops! Passwords go in password fields.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4475 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-23 21:29:01 +00:00
54898d3dbb More thorough checkpoint.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4474 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-23 18:49:24 +00:00
dd851a2b25 Let's try some wal truncating.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4473 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-23 18:41:58 +00:00
4c6b44eb30 Implemented password changing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4472 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-22 22:59:26 +00:00
74a3efe78d Let's restrict valid usernames.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4471 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-22 22:41:47 +00:00
51301fc49e More room robustifying?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4470 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-22 22:20:25 +00:00
02dd8c3dd0 Try to isolate my instability with go-ssb-room. I guess I did? Haven't found a resolution.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4469 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-21 23:38:55 +00:00
26a778c3b2 Might as well make the case consistent.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4468 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-21 00:40:47 +00:00
9fecbd97e8 Autocomplete blob refs. That's too cool.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4467 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-21 00:26:29 +00:00
e1383e3903 Move the HTTP timeout into C where we can manage it better as writes are active. Fixes an accidental 45 second GET timeout from httpd.js.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4466 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-20 23:30:29 +00:00
47532b8512 Snapping to grid.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4465 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-17 20:52:12 +00:00
3c4959433a Proof of concept of building emojis.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4464 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-17 13:22:29 +00:00
e921b4a86a Actually serialize doubles. Yikes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4463 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-17 00:13:31 +00:00
b23b0ca239 Some include-what-you-use progress.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4462 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-13 23:39:52 +00:00
191b45f054 Better map link.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4461 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-13 23:01:15 +00:00
15d0383349 api docs work in progress.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4460 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-13 23:00:47 +00:00
d2485583fd sqlite-amalgamation-3430100.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4459 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-13 21:19:37 +00:00
2b94704916 If a user visiting /login is already authenticated, bounce them away. This is me trying to avoid hassle for people who bookmarked the login page.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4458 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-11 16:52:17 +00:00
85ac6c215a Populate host and port for incoming SSB connections, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4457 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-09 14:45:55 +00:00
e83e665db9 Try harder to not reply to errors. Dunno.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4456 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-09 14:28:06 +00:00
645aafef16 zlib 1.3.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4455 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-07 22:44:49 +00:00
152c893a6f Fix wrong argument count on ssb.blobStore.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4454 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-07 22:44:18 +00:00
7c130dda56 Beef up the API docs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4453 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-07 00:40:43 +00:00
2d82dad806 Restore the connections tab.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4452 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-06 22:48:06 +00:00
e8ac5b759d Fix emoji search case again, and render issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4451 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-06 22:35:20 +00:00
4833d18968 Fix android app version.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4450 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-05 16:21:28 +00:00
6eafded1f6 Added the memory sparkline and tried to CSS them differently.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4449 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 20:13:17 +00:00
7b440b720e Set issues app icon.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4448 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 19:47:07 +00:00
e20ba7384f Put the latlng popup on the contextmenu event.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4447 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 19:26:48 +00:00
45231c6ede Add some GPS game tabs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4446 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 19:21:00 +00:00
35475defb5 Faster activity load through concurrency.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4445 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 18:01:06 +00:00
8741841f27 Add the missing leaflet .js.map file. Link to Google Maps from clicked map locations. Allow poup from the sandbox to support target="_blank" links, though that does not work here.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4444 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 17:44:05 +00:00
5282d19b55 Structure replies to issues as replies so that their posts don't appear so unusual.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4443 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-04 17:12:32 +00:00
d9782aa0fb Automatically reply encrypted to the same recipients.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4442 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-02 13:37:50 +00:00
9751facfb4 Add recps field to encrypted messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4441 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-02 13:25:31 +00:00
e0110203e7 More reliable addition of encrypted messages. There's something I'm not understanding with lit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4440 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-02 02:06:29 +00:00
088b44cc2c Fix the admin app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4439 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-01 16:26:24 +00:00
8f63bcbfbf Sending encrypted messages. Revealing some weird behavior, but it's working.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4438 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-09-01 01:34:01 +00:00
c8029388c9 Blockquote and connections tab tweaks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4437 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-31 16:46:40 +00:00
d9c4d847a1 Fix Strava account linking.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4436 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-31 16:31:24 +00:00
df9d9425ec Add issues app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4435 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-31 00:12:18 +00:00
90bb3c684e OK, no -fanalyzer on raspi, yet.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4434 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-30 23:58:01 +00:00
9c81b6de8a This should have gone away in a previous change.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4433 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-30 23:50:28 +00:00
6383498041 Oops. Makefile debugging.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4432 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-30 21:38:59 +00:00
daeb88785d Let's try -fanalyzer.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4431 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 20:57:55 +00:00
dcea08f73b Enforce a timeout on user SQL queries.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4430 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 20:23:40 +00:00
b252b921f8 Call out restricted DB access when we acquire the reader.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4429 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 19:41:54 +00:00
172826bf13 Use the right default port now that I'm not always running two different clients side-by-side.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4428 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 18:51:14 +00:00
060f1980f5 A little more paranoia around the file watcher.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4427 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 18:27:19 +00:00
e223d35252 Make the connections tab know more about tunnels and such.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4426 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 18:22:09 +00:00
99dba1a4c6 svn didn't notice that these files changed.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4425 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 17:19:59 +00:00
b52026c81f Give quoted text a bar on the side, like I'm used to.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4424 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 17:06:32 +00:00
47b8c86426 sqlite-amalgamation-3430000.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4423 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-25 16:20:32 +00:00
2e55c68648 Get ready for 0.0.11.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4422 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 22:40:00 +00:00
b7362dd84d 0.0.10.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4421 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 22:37:15 +00:00
01637b31e1 Let's release 0.0.10.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4420 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 21:55:22 +00:00
0e9a39608a Exclude gg from the release.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4419 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 21:54:43 +00:00
79404e4d41 Set android min sdk version and api version all to 28. That's all we need. Should fix reported crashes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4418 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 21:37:23 +00:00
35c21fbdaf No really, specify the API version for android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4417 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-23 02:16:22 +00:00
8c7bd7dc11 Fix windows build.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4416 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-22 16:49:42 +00:00
09ad4f0320 More callstacks on android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4415 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-22 16:48:12 +00:00
d96b836bef Better lifetimes still in the Java code, and turn on some strict vm policy messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4414 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-22 16:43:04 +00:00
59b2ffaf95 Ohh, Java does scoped resources.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4413 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-22 02:45:22 +00:00
f1b55ddd64 Attempt to track requests better. New requests need to be flagged as such. Still trying to chase tunnel instability.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4412 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-20 19:55:59 +00:00
85acac3a30 Save more context about closed connections, and include the timestamp.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4411 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-20 18:36:46 +00:00
befff5c1e5 Apps are not allowed to read directly from the blobs table.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4410 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-20 18:26:26 +00:00
d72ba81a67 Try to respond to tunnel errors I'm seeing instead of forwarding them over the tunnel, which obviously won't work. Allow creating multiple connections to the same ID if it's for the sake of a tunnel. I think this explains timeouts I'm seeing with tunnels. More error handling, too. C'mon, fix tunnels.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4409 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-20 18:25:15 +00:00
fef88e2032 Prevent the watcher's finalizer from being called before we're done with it.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4408 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-20 11:25:49 +00:00
20557e8ce4 Respond better when somebody disconnects from us with a tunnel. Trying to robustify tunnels. This is largely untested.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4407 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-17 16:54:33 +00:00
99c905e908 Fix build with the removal of split.js.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4406 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-17 00:50:22 +00:00
d7b58ee2c5 Support an edit-only mode, which brings up the editor without running the app. Helpful if the app hangs the client. Also on mobile, where both don't fit side-by-side.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4405 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-17 00:49:02 +00:00
faca2d387b Calculate thread busyness as the current concurrent running threads vs. the max number of threads ever seen running concurrently.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4404 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-17 00:01:59 +00:00
358d02d97f Another index I've wanted, and better error display for queries in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4403 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-16 22:57:16 +00:00
b66dac7465 Add an SQL query tab.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4402 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-16 22:48:59 +00:00
f7d201859a Use the sqlite authorizer for async requests, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4401 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-16 22:43:08 +00:00
61d2ef5469 Yeah, the ping comes in a later message. Whoops.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4400 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-15 23:56:58 +00:00
ac994b9c62 Were we not responding to ping?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4399 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-14 16:26:06 +00:00
264dcbc331 codemirror 5.65.14.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4398 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-13 01:11:14 +00:00
e5425c0ffb Apparently the MUXRPC maximum segment size is 4096: bd350c6f9e/boxstream/box.go (L23). Reducing the send size seems to keep me connected to/through rooms longer.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4397 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-12 19:57:00 +00:00
e10803de68 Fix GPX upload.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4396 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-12 18:37:15 +00:00
07b1a0e403 Fiddling with login CSS.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4395 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-12 11:54:27 +00:00
6ed2c702d8 Hide too-new messages, and cycle between message, raw, and markdown views of messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4394 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-09 23:39:17 +00:00
5c1c33d33e Put the release process in the makefile, and including building the tarball contents to avoid another snafu.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4393 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-09 23:14:16 +00:00
70d37c88b5 Redo auth flow with lit. Beef up the test a bit, accordingly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4392 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-09 22:38:41 +00:00
1ba37d95b5 More concise wait.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4391 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-07 16:13:02 +00:00
0d82198849 Remove old, broken drag+drop code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4390 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-06 12:03:37 +00:00
39927e75f2 Attempt to support .gpx files.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4389 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-06 12:03:22 +00:00
e6fd33b969 Sure, let's add GPS game.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4388 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-06 00:26:56 +00:00
e8fe32d5af Fix a crash on android three different ways?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4387 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-05 03:10:24 +00:00
bfc8bb864d I suspect this is necessary to prevent sending an error when we get the final response to blobs.get.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4386 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-05 02:30:45 +00:00
9179746763 Freaking CSS. Trying to make the admin page...work.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4385 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-05 01:22:27 +00:00
d0177d24cb Clean up some test cruft.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4384 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 23:44:48 +00:00
0573008c9c Set some blob auto-delete option defaults on android only.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4383 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 23:35:02 +00:00
9506f518c2 +x and shebang
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4382 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 23:33:57 +00:00
0f0ae9153b lit-html 2.8.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4381 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 23:10:53 +00:00
09c7c8ac64 GPS game.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4380 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 23:08:16 +00:00
5e2dfff148 Remembering permissions never worked???
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4379 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-04 00:50:13 +00:00
958b47548d There, I can wait for a thing in a shadow root like I want.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4378 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-03 00:44:35 +00:00
16155ef746 Automated enough with selenium to be able to create a Tilde Friends account, create an SSB identity, and post a first message. I'm still confused on some things, but this is progress, and I fixed a longstanding issue creating the first identity.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4377 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-03 00:30:48 +00:00
5755b61ea6 Oops, one more reference to smoothie.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4376 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-08-02 16:25:41 +00:00
353847a77f Remove the smoothie graphs. The sparklines are too good. I will rebuild whatever I'm missing with these.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4375 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-31 16:32:21 +00:00
bdf64edeb8 Expose the client's requesting URL to apps.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4374 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-31 00:26:09 +00:00
b5768dd927 Capture (almost) all worker thread time.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4373 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-29 22:29:09 +00:00
3e5abf3a4d Enable auto vacuum.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4372 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 12:22:37 +00:00
d3029639de We can't exclude libsodium's version.h!
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4371 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 12:22:16 +00:00
d21d7e4add Delete more aggressively.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4370 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 03:04:49 +00:00
afde69b5d9 Took a whack at cleaning up old blobs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4369 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 02:51:42 +00:00
3319df3df0 Oh yeah, I was playing with the follow app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4368 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 01:47:50 +00:00
1102feaac3 speedscope 1.16.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4367 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 00:42:51 +00:00
deede728be Now we're 0.0.10-wip.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4366 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 00:37:09 +00:00
fc3dd84122 Let's release 0.0.9.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4365 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-27 00:00:53 +00:00
9239441d73 Fixed duplicate tags.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4364 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-26 23:56:40 +00:00
b984811851 Don't shutdown the client side of an HTTP request after sending it. Some servers don't like that.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4363 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-23 01:23:44 +00:00
1c52446331 Use picohttpparser for responses, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4362 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-23 01:12:11 +00:00
b6dffa8e66 Actually return the blob ID from store.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4361 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-22 01:33:28 +00:00
315d650d27 Same bug twice.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4360 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-22 01:33:06 +00:00
07c121044a Fix a crash uploading blobs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4359 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-22 01:24:58 +00:00
f3169afcf5 Do a silly thing to show dependency versions.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4358 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-20 05:15:44 +00:00
c371fc2a8e Fixed multiple trace problems.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4357 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-20 05:06:15 +00:00
6889e11fd1 Minor cleanup. Missing traces.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4356 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-20 02:20:38 +00:00
fb73fd0afc Make storing messages async. Phew.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4355 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-20 01:02:50 +00:00
6fcebd7a08 Nope, do the thing from the right thread.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4354 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-19 00:58:20 +00:00
15ea62a546 Trace all the async things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4353 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-18 23:56:20 +00:00
b0cd58f5aa Make blob store actually not block the main thread.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4352 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-18 23:46:15 +00:00
7fe8f66fd3 Yikes. I broke appending?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4351 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-18 00:59:25 +00:00
68ca99e9d9 Remove some unnecessary code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4350 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-18 00:52:08 +00:00
a2542c658b Better tag enumerating.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4349 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-16 23:41:41 +00:00
eb203c7e62 Don't put a JWT in core.user.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4348 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-16 22:03:47 +00:00
6ef466f3ed Fixed enough thing sto be able to authenticate and get data from Strava.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4347 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-16 21:04:48 +00:00
5074246462 Listening on IPv6 + IPv4 by default.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4346 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-16 14:04:45 +00:00
73bbcebddb Brushing off enough dust to be able to initiate HTTP requests from the server.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4345 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-15 01:48:36 +00:00
18128303b6 Appending a message produces the ID. And bump the version.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4344 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-13 00:20:12 +00:00
c4a2d790a3 Expose creds to request handlers.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4343 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-13 00:00:41 +00:00
c1ec150696 SHA256 was sticking out on a profile, so don't unnecessarily hold the DB writer while we're doing that.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4342 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-08 13:43:44 +00:00
f4b856df15 Expose parsed query args to request handlers.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4341 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-08 13:33:34 +00:00
85b87553dd Avoid SQL logic error in blob replication.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4340 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-07 12:08:14 +00:00
5decdf3afa Better tags query.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4339 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-06 00:37:16 +00:00
a4acee4939 Fix a stall where we would process one scheduled task and then leave the rest until we wake up again from network traffic.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4338 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-06 00:35:39 +00:00
d06aea2831 Expose versions of dependencies.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4337 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-05 01:06:59 +00:00
ae0a8b0a33 libuv 1.46.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4336 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-07-04 00:24:48 +00:00
f0452704a1 speedscope-1.15.2.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4335 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-29 00:25:25 +00:00
b8b1f1ba80 Confused by this message. Add more context.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4334 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-29 00:17:32 +00:00
caf7478da4 Ugg, release .apk pls.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4333 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-28 23:29:56 +00:00
0e40ba78a4 Update lit to 2.7.5, and make building the .apk part of the release.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4332 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-28 23:28:59 +00:00
d1eac6c9eb Hook up android version numbers, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4331 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-28 23:23:29 +00:00
8f5201b2bc Show a version number in the UI. Automate things so that the version number originates from the Makefile. Get ready for 0.0.8.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4330 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-28 23:00:34 +00:00
6022001d66 Primitive display of recent channels/tags and the same on messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4329 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-22 00:27:27 +00:00
f018c367ed Don't automatically add mentions for incomplete &/@/% links.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4328 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-17 14:30:17 +00:00
48c47f097a This seems to fix losing sizes when attaching files.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4327 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-17 14:23:32 +00:00
39ac215b5a Store blobs from the worker threads. Let's see if this is a good idea.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4326 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-17 14:05:23 +00:00
7d562ce85c Allow the DB writer to be used from a worker thread. Not well tested, just still trying to charge forward on moving all blocking work off the main thread.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4325 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-15 00:27:49 +00:00
51b317233a First rough-out of a mentions tab in the SSB app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4324 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-14 22:51:58 +00:00
87ce715011 This appears to let me shrink the sparkline graphs. Freaking CSS.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4323 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-14 22:23:22 +00:00
ef5afc1e23 Minor cleanup while pondering syncing faster.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4322 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-14 21:59:04 +00:00
486212f22a Fix expanding messages on the search tab. Maybe this should happen at another level.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4321 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-14 16:39:08 +00:00
0e8867dd6e Attempt to tie subprocess lifetime to the android activity.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4320 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-08 00:51:34 +00:00
ca28b5ca82 Delete some code that doesn't need to exist.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4319 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-01 22:53:44 +00:00
19e26c1759 Support setting publicWebHosting, and kill some unused code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4318 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-01 22:21:14 +00:00
790f6643a4 Mostly fumbling with error handling.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4317 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-27 16:51:56 +00:00
2158ad3c0b sqlStream => sqlAsync
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4316 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-24 00:10:05 +00:00
d904d8922f Oops, no verbose.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4315 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:36:21 +00:00
da50792500 Avoid chunked content encoding. Some WebViewClient debugging. Doesn't go to a blank screen on android so much.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4314 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:26:07 +00:00
b4629acc48 Ugg. libuv and io_uring and android: https://github.com/libuv/libuv/issues/4010.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4313 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:06:59 +00:00
0cf4118330 Remove Socket.info.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4312 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:47:25 +00:00
dd61a6ecc3 Report which method was not found.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4311 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:16:07 +00:00
8e6f1284e1 Show the edit pane before it finishes loading so that it's more clear it's working.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4310 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:03:17 +00:00
813d3cd492 Lit Element 2.7.4.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4309 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-21 21:46:32 +00:00
f421606e21 libuv 1.45.0, #include cleanup, probably something else.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4308 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-21 21:36:51 +00:00
1ccb9183b4 Don't mess with websockets when we're returning a document from an app's handler.js.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4307 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-19 19:57:40 +00:00
7d9b627f37 Report attempts to call tfrpc methods that aren't registered.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4306 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-19 19:47:33 +00:00
3038138909 This is a sketch of a setup that allows apps to produce sandboxed dynamic content without all the iframe/websocket business.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4305 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 20:22:13 +00:00
2ca08d21e4 I broke magic byte detection, and missed some Content-Security-Policy opportunities.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4304 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:57:56 +00:00
478e96fc5f Just moving HTTP code around.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4303 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:35:58 +00:00
e237c7ea1d Remove valgrind hooks. In this house, we use asan and custom allocators. Smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4302 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:24:10 +00:00
bf9ff088fd Handle unsuccessfully decrypted messages, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4301 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 16:57:43 +00:00
e073ebedd1 sqlite-amalgamation-3420000.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4300 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 14:15:29 +00:00
10d4ae7dcc Decrypt messages in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4299 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 14:10:49 +00:00
5b8bdbb3e4 Today I discovered the "Content-Security-Policy: sandbox" header.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4298 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 19:46:01 +00:00
c807e21c6b Don't let browsers render untrusted HTML or SVG outside of the iframe. Do let them fetch JS and such.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4297 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 19:31:45 +00:00
cc92d0e316 Simplify magic bytes lookup slightly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4296 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:47:19 +00:00
09c396d5a3 Default the files panel to expanded.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4295 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:05:28 +00:00
bc5bbca951 Remove importing and export from the ssb app. I like it better as the separate sneaker app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4294 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:02:56 +00:00
ed4faedcd7 Report some information when importing messages and discover an old verification bug.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4293 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-11 00:22:42 +00:00
251556ebed Sneakernet, here we come.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4292 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 23:52:46 +00:00
1324afb459 Zip export still had stringified content.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4291 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:52:34 +00:00
1119804fc2 Whitespace.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4290 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:47:58 +00:00
cdf6440197 Uncommit unintended part of previous change.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4289 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:47:20 +00:00
8727fe00af Return something from ssb.storeMessage.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4288 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:45:37 +00:00
7da7890bb6 Work in progress zip import/export.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4287 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:30:15 +00:00
706bd2c51f Save some space + more deterministic with relative paths for debug info.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4286 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:44:32 +00:00
acabec940e Make emojis.json much smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4285 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:32:50 +00:00
470b998b61 Add a vector launcher icon. Currently the smiling face with sunglasses emoji from openmoji.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4284 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:04:43 +00:00
80fad05f23 Show latest value on the spark line graphs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4283 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 23:37:02 +00:00
07a912fb9a Files pane => lit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4282 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 23:12:34 +00:00
e9d83262c4 Sparkline graph tweaks. Minor cleanup.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4281 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 22:47:00 +00:00
74323c22f9 I think this lets me load more pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4280 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 22:32:21 +00:00
2614e89b1b Actually update to lit 2.7.3.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4279 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-02 16:50:26 +00:00
e092fe1399 Updated lit, starting to improve the display of mentions during editing, . to refresh, and probably some other things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4278 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-02 16:47:27 +00:00
9cbe895cb8 Exclude .map files from the APK to squeeze them under the blob size limit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4277 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 13:24:01 +00:00
b0b0f74e83 Eek out a little more space on Android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4276 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 12:17:13 +00:00
d9eaa92c37 Messing with graph sizing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4275 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 12:00:50 +00:00
566d07117e Fix the android build.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4274 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 11:48:16 +00:00
2bffdb1168 Thought I had a fundamental UDP broadcast problem, but it was just bad setup in the test.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4273 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 03:18:12 +00:00
1359b48c9f Turn on -Wdouble-promotion. Why not.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4272 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 03:00:57 +00:00
a69fb5eeac I think this fixes posting.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4271 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 00:56:59 +00:00
38e313350e Trying to make the navigation bar resize right, but CSS doesn't like me.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4270 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 20:49:06 +00:00
5052dc04f2 Added spark line emojis and fixed some things about their rendering.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4269 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 19:46:33 +00:00
9ef3a3aca0 An experiment: Always show some stats as little sparklines at the top of the screen.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4268 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 19:27:00 +00:00
7b91a2ec37 Navigation bar => lit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4267 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 18:23:08 +00:00
2926f855a1 Start using lit element in the main web interface. It's getting out of control, and if I can finish a refactor, it will reel it in.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4266 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 16:52:35 +00:00
639419db60 Oh freaking heck. This fixes the black bar at the bottom of the screen on Android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4265 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-20 00:24:12 +00:00
54747c127c Ugg. Android needs File.write.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4264 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-20 00:11:38 +00:00
791c3dd787 Remove unused file operations.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4263 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:53:52 +00:00
b00d75ab7c Fine. Fix windows.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4262 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:06:37 +00:00
956ea0df56 Track and expose hitches in some suspect callbacks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4261 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:05:59 +00:00
30014040e7 Update lit-all.min.js for ssb.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4260 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-16 21:36:17 +00:00
ab055c3394 I see what happened. codemirror 6.57.7 was really a misnumbered codemirror5 release. Let's go back to the latest codemirror5.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4259 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-16 13:07:02 +00:00
1e37eeea05 Experimenting with collapsing images.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4258 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-13 00:03:22 +00:00
84aec0278d Free earlier.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4257 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:28:58 +00:00
06642f58c5 One less blocking thing on the main thread: _tf_ssb_connection_send_history_stream.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4256 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:22:33 +00:00
e6d44b32f4 Seems we no longer need _tf_ssb_followingDeep.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4255 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:06:56 +00:00
1f3f6e2b92 Show audio: references inline, too, and now we don't have to show audio: and video: in the references section.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4254 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 00:32:14 +00:00
8f2d3e3bcd Show videos in messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4253 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-08 20:06:45 +00:00
2df2fc5792 This appears to avoid webview state loss when rotating.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4252 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:43:41 +00:00
20b0337e0a Hook up backtraces on android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4251 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:25:17 +00:00
e86b9dae48 Lint cleanup.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4250 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:02:12 +00:00
71de897419 Missing icon.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4249 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 14:33:52 +00:00
3edfaf9137 Add/enable codemirror's javascript-lint using jshint, and fix a few things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4248 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 00:46:40 +00:00
19c1784864 sqlite-amalgamation-3410200.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4247 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 00:13:39 +00:00
16606 changed files with 71263 additions and 469539 deletions

124
Makefile
View File

@ -3,6 +3,10 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 11
VERSION_NUMBER := 0.0.11
VERSION_NAME := Be nothing, and you will have everything to give to others.
PROJECT = tildefriends
BUILD_DIR ?= out
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
@ -24,8 +28,7 @@ ANDROID_SDK ?= ~/Android/Sdk
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
ANDROID_NDK_API_VERSION := 31
ANDROID_MIN_SDK_VERSION := 26
ANDROID_MIN_SDK_VERSION := 28
ANDROID_ARM64_TARGETS := \
out/androiddebug/tildefriends \
@ -56,8 +59,10 @@ $(NONANDROID_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
-fno-asynchronous-unwind-tables \
-funwind-tables
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
@ -76,10 +81,10 @@ windebug winrelease: LDFLAGS += \
-Ldeps/openssl/mingw64/lib
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang
$(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-Wno-unknown-warning-option
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
@ -114,7 +119,12 @@ $(APP_OBJS): CFLAGS += \
-Ideps/sqlite \
-Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \
-Werror
ifeq ($(UNAME_M),x86_64)
$(filter-out $(BUILD_DIR)/android%,$(APP_OBJS)): CFLAGS += \
-fanalyzer
endif
BLOWFISH_SOURCES := \
deps/crypt_blowfish/crypt_blowfish.c \
@ -140,13 +150,10 @@ UV_SOURCES_unix := \
deps/libuv/src/unix/async.c \
deps/libuv/src/unix/core.c \
deps/libuv/src/unix/dl.c \
deps/libuv/src/unix/epoll.c \
deps/libuv/src/unix/fs.c \
deps/libuv/src/unix/getaddrinfo.c \
deps/libuv/src/unix/getnameinfo.c \
deps/libuv/src/unix/linux-core.c \
deps/libuv/src/unix/linux-inotify.c \
deps/libuv/src/unix/linux-syscalls.c \
deps/libuv/src/unix/linux.c \
deps/libuv/src/unix/loop-watcher.c \
deps/libuv/src/unix/loop.c \
deps/libuv/src/unix/pipe.c \
@ -164,7 +171,6 @@ UV_SOURCES_unix := \
deps/libuv/src/unix/tty.c \
deps/libuv/src/unix/udp.c
UV_SOURCES_android := \
deps/libuv/src/unix/pthread-fixes.c \
deps/libuv/src/unix/random-getentropy.c
UV_SOURCES_win := \
deps/libuv/src/win/async.c \
@ -196,12 +202,13 @@ UV_OBJS := $(call get_objs,UV_SOURCES)
$(UV_OBJS): CFLAGS += \
-Ideps/libuv/include \
-Ideps/libuv/src \
-Wno-unused-but-set-variable \
-Wno-incompatible-pointer-types \
-Wno-sign-compare \
-Wno-unused-variable \
-Wno-dangling-pointer \
-Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \
-Wno-sign-compare \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable \
-D_GNU_SOURCE
SODIUM_SOURCES := \
@ -244,7 +251,8 @@ SODIUM_SOURCES := \
deps/libsodium/src/libsodium/sodium/core.c \
deps/libsodium/src/libsodium/sodium/codecs.c \
deps/libsodium/src/libsodium/sodium/runtime.c \
deps/libsodium/src/libsodium/sodium/utils.c
deps/libsodium/src/libsodium/sodium/utils.c \
deps/libsodium/src/libsodium/sodium/version.c
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
$(SODIUM_OBJS): CFLAGS += \
-DCONFIGURED=1 \
@ -253,6 +261,7 @@ $(SODIUM_OBJS): CFLAGS += \
-Wno-unused-variable \
-Wno-type-limits \
-Wno-unknown-pragmas \
-Ideps/libsodium/builds/msvc \
-Ideps/libsodium/src/libsodium/include/sodium
SQLITE_SOURCES := deps/sqlite/sqlite3.c
@ -265,7 +274,7 @@ $(SQLITE_OBJS): CFLAGS += \
-DSQLITE_ENABLE_FTS5 \
-DSQLITE_ENABLE_JSON1 \
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
-DSQLITE_MAX_ATTACHED=0 \
-DSQLITE_MAX_ATTACHED=1 \
-DSQLITE_MAX_COLUMN=100 \
-DSQLITE_MAX_COMPOUND_SELECT=300 \
-DSQLITE_MAX_EXPR_DEPTH=40 \
@ -374,17 +383,19 @@ debug release: LDFLAGS += \
-lssl \
-lcrypto
windebug winrelease: LDFLAGS += \
-lwsock32 \
-lws2_32 \
-lkernel32 \
-liphlpapi \
-luserenv \
-lssl \
-lcrypto \
-lcrypt32 \
-ldbghelp \
-liphlpapi \
-lkernel32 \
-lole32 \
-luserenv \
-luuid \
-lws2_32 \
-lcrypt32
-lwsock32
$(ANDROID_TARGETS): LDFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-ldl \
-llog \
-lssl \
@ -431,15 +442,32 @@ endef
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
src/version.h : $(firstword $(MAKEFILE_LIST))
@echo [version] $@
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"\n#define VERSION_NAME \"$(VERSION_NAME)\"\n" > $@
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
@echo [android_version] $@
@sed -i \
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
-e 's/android:minSdkVersion=".*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
$@
# Android support.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat src/android/AndroidManifest.xml
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@mkdir -p $(dir $@)
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
@echo [aapt2] $@
@$(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/
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)))
@ -457,8 +485,7 @@ PACKAGE_DIRS := \
apps/ \
core/ \
deps/codemirror/ \
deps/split/ \
deps/smoothie/
deps/lit/
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
@ -478,20 +505,57 @@ out/%.unsigned.apk:
@cp out/apk/res.apk $@
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 -r $(PACKAGE_DIRS) $(RAW_FILES)
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
out/%.apk: out/apk/%.unsigned.apk
@echo [apksigner] $(notdir $@)
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
apk: out/TildeFriends-debug.apk
apk: out/TildeFriends-release.apk
.PHONY: apk
apkgo: out/TildeFriends-debug.apk
apkgo: out/TildeFriends-release.apk
@adb install $<
@adb shell am start com.unprompted.tildefriends/.MainActivity
.PHONY: apkgo
apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
dist: apk
@echo "[export] $$(svn info --show-item url)"
@rm -rf tildefriends-$(VERSION_NUMBER)
@svn export -q . tildefriends-$(VERSION_NUMBER)
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
@tar \
--exclude=apps/gg* \
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
--exclude=deps/libsodium/builds/msvc/vs* \
--exclude=deps/libsodium/builds/msvc/build \
--exclude=deps/libsodium/builds/msvc/properties \
--exclude=deps/libsodium/configure \
--exclude=deps/libsodium/test \
--exclude=deps/libuv/docs \
--exclude=deps/libuv/test \
--exclude=deps/openssl \
--exclude=deps/speedscope/*.map \
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER)
@rm -rf tildefriends-$(VERSION_NUMBER)
@echo "[cp] TildeFriends-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-release.apk TildeFriends-$(VERSION_NUMBER).apk
.PHONY: dist
dist-test: dist
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
@rm -rf tildefriends-$(VERSION_NUMBER)
.PHONY: dist-test

View File

@ -9,14 +9,18 @@ tfrpc.register(function global_settings_set(key, value) {
});
async function main() {
let data = {
users: {},
granted: await core.allPermissionsGranted(),
settings: await core.globalSettingsDescriptions(),
};
for (let user of await core.users()) {
data.users[user] = await core.permissionsForUser(user);
try {
let data = {
users: {},
granted: await core.allPermissionsGranted(),
settings: await core.globalSettingsDescriptions(),
};
for (let user of await core.users()) {
data.users[user] = await core.permissionsForUser(user);
}
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
} catch {
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
}
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
}
main();

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<html>
<html style="width: 100%">
<head>
<script>const g_data = $data;</script>
</head>
<body style="color: #fff">
<body style="color: #fff; width: 100%">
<h1>Tilde Friends Administration</h1>
</body>
<script type="module" src="script.js"></script>

View File

@ -25,29 +25,37 @@ window.addEventListener('load', function() {
function input_template(key, description) {
if (description.type === 'boolean') {
return html`
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
<span>${description.description}</span>
<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>
`;
} else if (description.type === 'textarea') {
return html`
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
<span>${description.description}</span>
<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>
`;
} else {
return html`
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
<span>${description.description}</span>
<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>
`;
}
@ -67,12 +75,13 @@ window.addEventListener('load', function() {
${Object.entries(users).map(u => user_template(u[0], u[1]))}
</ul>`;
const page_template = (data) =>
html`<div>
<h2>Global Settings</h2>
<div style="display: grid">
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
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])}`)}
</div>
${users_template(data.users)}
</div>
${users_template(data.users)}
</div>`;
`;
render(page_template(g_data), document.body);
});

File diff suppressed because one or more lines are too long

313
apps/api/docs.js Normal file
View File

@ -0,0 +1,313 @@
export const docs = {};
docs.global = `# Tilde Friends API Documentation
Welcome to the Tilde Friends API documentation.
* [App Globals](#App_Globals)
* [Database Interface](#Database)
* [Remote Procedure Calls](#tfrpc)
<a id="App_Globals"></a>
## <span style="color: #aaf">App Globals</span>
The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`. Most
of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless
noted otherwise.
This is all a work in progess. These are liable to change without warning. Feedback is welcome.
The exposed functions in this API balance multiple competing needs:
* The surface area of the exposed API ought to be fairly minimal. If something can be implemented entirely app-side, that is
generally preferred over building it into the core.
* Everything is built on this API. Ideally the admin app, the SSB app, and the editor all use standard API exposed to all
apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations.
There will be some things here that aren't necessarily general use to support what's required.
If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/),
the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`.
`;
docs['core.user.credentials.session.name'] = `
*String* The name of the authenticated user.
`;
docs['app.setDocument()'] = `
Set the contents of the client &lt;iframe/&gt;.
### Parameters
* *String* **html** The HTML contents.
`;
docs['ssb.sqlAsync()'] = `
Run an SQL query against the sqlite database.
### Parameters
* *String* **query** The sqlite query.
* *Array* **args** The query arguments to bind.
* *Function* **callback** Callback called for each row result.
`;
docs['ssb.appendMessageWithIdentity()'] = `
Signs and stores a message in the SSB database.
### Parameters
* *String* **id** The public key of an SSB identity owned by the authenticated user.
* *Object* **message** The unsigned message.
`;
docs['ssb.storeMessage()'] = `
Verifies and stores a signed message in the SSB database.
### Parameters
* *Object* **message** The valid, signed message to store.
`;
docs['ssb.blobStore()'] = `
Store a blob in the SSB database.
### Parameters
* *String*/*Uint8Array* **blob** The blob contents to store
### Returns
*String* The stored blob ID.
`;
docs['ssb.blobGet()'] = `
Fetches a blob from the database.
### Parameters
* *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
### Returns
*ArrayBuffer* The blob data.
`;
docs['print()'] = `
Log debug information both to the server's console and to the visiting user's browser console when possible.
### Parameters
* **...** Whatever you want to log. Will be joined with spaces.
`;
docs['database()'] = `
Returns a database instance that is specific to the authenticated user and the given key.
### Parameters
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['my_shared_database()'] = `
Returns a database instance that is specific to the authenticated user and the given key.
### Parameters
* *String* **package_name** The database package name.
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['shared_database()'] = `
Returns a database instance that is shared between all users of the app, determined by its owner and app name.
### Parameters
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['base64Decode()'] = `
Decode a base64 string to bytes.
Completes synchronously.
### Parameters
* *String* value The base64-encoded string.
### Returns
*Uint8Array* The decoded bytes.
`;
docs['base64Encode()'] = `
Encode bytes to a base64 string.
Completes synchronously.
### Parameters
* *Uint8Array* The bytes to encode.
### Returns
*String* The base64-encoded string.
`;
docs['utf8Decode()'] = `
Decode UTF-8 bytes to a string.
Completes synchronously.
### Parameters
* *Uint8Array* **value** The value to decode.
### Returns
*String* The value as a string.
`;
docs['utf8Encode()'] = `
Encodes a string to UTF-8 bytes.
Completes synchronously.
### Parameters
* *String* **value** The value to encode.
### Returns
*Uint8Array* The encoded \`value\`.
`;
docs['setTimeout()'] = `
Call a function after some delay.
### Parameters
* *Function* **callback** The function to call.
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
`;
docs['parseHttpRequest()'] = `
Parses an HTTP request.
### Parameters
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
### Returns
* *Integer* **-2** if the request is incomplete.
* *Integer* **-1** if the request could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
`;
docs['parseHttpResponse()'] = `
Parses an HTTP response.
### Parameters
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
### Returns
* *Integer* **-2** if the response is incomplete.
* *Integer* **-1** if the response could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
`;
docs['sha1Digest()'] =`
Calculates a SHA1 digest.
Completes synchronously.
### Parameters
* *String* **value** The value for which to calculate the digest.
### Returns
*String* The SHA1 digest of UTF-8 encoded \`value\`.
`;
docs['maskBytes()'] = `
Masks bytes for WebSocket communication.
Completes synchronously.
### Parameters
* *Uint8Array* **bytes** The byte array of data to mask.
* *Uint32* **mask** The mask to apply.
### Returns
*Uint32Array* The masked bytes.
`;
docs['exit()'] = `
Exits the app. But why would you want to do that?
Completes synchronously.
### Parameters
* *Integer* **exit_code** System exit code.
`;
docs['version()'] = `
Gets version information for the running server.
### Returns
*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
dependencies. Values are *String* version numbers.
`;
docs['platform()'] = `
Gets the host operating system platform of the running server.
### Returns
*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
`;
docs['getFile()'] = `
Gets a file from the running app.
### Parameters
* *String* **name** Name of the file to retrieve.
### Returns
*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
`;
docs.database = `
# <span style="color: #aaf">Database</span>
Local-only storage is provided by a \`Database\` type representing a key-value store.
`;
docs['database.get()'] = `
Gets a value from the database.
### Parameters
* *String* **key** The key.
### Returns
*String* The value from the database or undefined if not found.
`;
docs['database.getAll()'] = `
Gets all keys from the database.
### Returns
*Array* An array of *String* key names for all keys in the given database.
`;
docs['database.getLike()'] = `
Gets all keys and values from the database matching a pattern.
### Parameters
* *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
### Returns
*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
`;
docs['database.set()'] = `
Sets a value in the database, creating a new entry or replacing an existing entry.
### Parameters
* *String* **key** The key.
* *String* **value** The value.
`;
docs['database.exchange()'] = `
Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
### Parameters
* *String* **key** The key.
* *String* **expected** The expected value.
* *String* **value** The new value.
### Returns
*Boolean* true if the value is now the given value.
`;
docs['database.remove()'] = `
Removes an entry from the database if it exists.
### Parameters
* *String* **key** The key.
`;
docs.tfrpc = `
# <span style="color: #aaf" id="tfrpc">tfrpc</span>
\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
and the app process on the server.
From \`app.js\`:
\`\`\`
import * as tfrpc from '/tfrpc.js';
\`\`\`
From script running in the browser:
\`\`\`
import * as tfrpc from '/static/tfrpc.js';
\`\`\`
Either side can register or call functions, though they must be registered before they can be called. Arguments and return
values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
`;
docs['tfrpc.register()'] = `
Register a function, allowing it to be called remotely.
### Parameters
* *Function* **function** The function to register. Its name will be how it will be called.
`;
docs['tfrpc.rpc.*()'] = `
Call a remote function.
### Parameters
* **...** Parameters to pass to the function.
### Returns
The return value of the called function.
`;

View File

@ -70,7 +70,7 @@ async function main() {
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
</script>
</html>`
</html>`;
app.setDocument(doc);
}

View File

@ -1,3 +1,4 @@
{
"type": "tildefriends-app"
"type": "tildefriends-app",
"emoji": "🛍"
}

View File

@ -1,6 +1,6 @@
async function get_apps() {
let results = {};
await ssb.sqlStream(`
await ssb.sqlAsync(`
SELECT messages.*
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
@ -52,4 +52,4 @@ async function main() {
`);
}
main();
main();

View File

@ -20,7 +20,7 @@ async function database_list() {
}
populate_dbs('dbs', ${JSON.stringify(dbs)});
</script>
</html>`
</html>`;
app.setDocument(doc);
}
@ -47,7 +47,7 @@ async function key_list(db) {
}
populate_dbs('keys', ${JSON.stringify(object)});
</script>
</html>`
</html>`;
app.setDocument(doc);
}

View File

@ -16,9 +16,7 @@
- / => Something good.
- update docs
- audit + document API exposed to apps
- sqlStream => sqlExec or something
- fix weird HTTP warnings
- ssb from child process?
- channels
- placeholder/missing images
- no denial of service
@ -57,7 +55,9 @@
- keep working on good error feedback
- build for windows
- installable apps (bring back an app message?)
- sqlStream => sqlExec or something
- !ssb from child process?
## Done
- update LICENSE
- logging to browser
- logging to browser

View File

@ -1,75 +1,163 @@
"use strict";
let g_about_cache = {};
var g_following_cache = {};
var g_following_deep_cache = {};
var g_about_cache = {};
async function following(db, id) {
if (g_following_cache[id]) {
return g_following_cache[id];
}
var o = await db.get(id + ":following");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], sequence: 0, version: k_version};
}
f.users = new Set(f.users);
await ssb.sqlAsync(
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
" json_extract(content, '$.following') AS following "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'contact' "+
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
f.sequence = row.sequence;
});
var as_set = f.users;
f.users = Array.from(f.users).sort();
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":following", j);
}
f.users = as_set;
g_following_cache[id] = f.users;
return f.users;
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function(row) {
result.push(row);
});
return result;
}
async function followingDeep(db, seed_ids, depth) {
if (depth <= 0) {
return seed_ids;
async function contacts_internal(id, last_row_id, following, max_row_id) {
let result = Object.assign({}, following[id] || {});
result.following = result.following || {};
result.blocking = result.blocking || {};
let contacts = await query(
`
SELECT content FROM messages
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, '$.type') = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]);
for (let row of contacts) {
let contact = JSON.parse(row.content);
if (contact.following === true) {
result.following[contact.contact] = true;
} else if (contact.following === false) {
delete result.following[contact.contact];
} else if (contact.blocking === true) {
result.blocking[contact.contact] = true;
} else if (contact.blocking === false) {
delete result.blocking[contact.contact];
}
}
var key = JSON.stringify([seed_ids, depth]);
if (g_following_deep_cache[key]) {
return g_following_deep_cache[key];
following[id] = result;
return result;
}
async function contact(id, last_row_id, following, max_row_id) {
return await contacts_internal(id, last_row_id, following, max_row_id);
}
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
let result = {};
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let contact = contacts[i];
let all_blocking = Object.assign({}, contact.blocking, blocking);
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
result[id] = [id, ...found, ...deeper];
}
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
var ids = [].concat(...f);
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
x = [...new Set([].concat(...x, ...seed_ids))].sort();
g_following_deep_cache[key] = x;
return x;
return [...new Set(Object.values(result).flat())];
}
async function following_deep(ids, depth, blocking) {
let db = await database('cache');
const k_cache_version = 5;
let cache = await db.get('following');
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
version: k_cache_version,
following: {},
last_row_id: 0,
};
}
let max_row_id = (await query(`
SELECT MAX(rowid) AS max_row_id FROM messages
`, []))[0].max_row_id;
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
cache.last_row_id = max_row_id;
let store = JSON.stringify(cache);
await db.set('following', store);
return result;
}
async function fetch_about(db, ids, users) {
const k_cache_version = 1;
let cache = await db.get('about');
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
version: k_cache_version,
about: {},
last_row_id: 0,
};
}
let max_row_id = 0;
await ssb.sqlAsync(`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[],
function(row) {
max_row_id = row.max_row_id;
});
for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) {
delete cache.about[id];
}
}
let abouts = [];
await ssb.sqlAsync(
`
SELECT
messages.*
FROM
messages,
json_each(?1) AS following
WHERE
messages.author = following.value AND
messages.rowid > ?3 AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
UNION
SELECT
messages.*
FROM
messages,
json_each(?2) AS following
WHERE
messages.author = following.value AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
ORDER BY messages.author, messages.sequence
`,
[
JSON.stringify(ids.filter(id => cache.about[id])),
JSON.stringify(ids.filter(id => !cache.about[id])),
cache.last_row_id,
max_row_id,
]);
for (let about of abouts) {
let content = JSON.parse(about.content);
if (content.about === about.author) {
delete content.type;
delete content.about;
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
}
}
cache.last_row_id = max_row_id;
await db.set('about', JSON.stringify(cache));
users = users || {};
for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(users[id] || {}, cache.about[id]);
}
return Object.assign({}, users);
}
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
var o = await db.get(id + ":about");
let o = await db.get(id + ":about");
const k_version = 4;
var f = o ? JSON.parse(o) : o;
let f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
@ -89,7 +177,7 @@ async function getAbout(db, id) {
function(row) {
f.sequence = row.sequence;
if (row.content) {
var about = {};
let about = {};
try {
about = JSON.parse(row.content);
} catch {
@ -99,7 +187,7 @@ async function getAbout(db, id) {
f.about = Object.assign(f.about, about);
}
});
var j = JSON.stringify(f);
let j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":about", j);
}
@ -110,7 +198,7 @@ async function getAbout(db, id) {
async function getSize(db, id) {
let size = 0;
await ssb.sqlAsync(
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
[id],
function (row) {
size += row.size;
@ -118,6 +206,25 @@ async function getSize(db, id) {
return size;
}
async function getSizes(ids) {
let sizes = {};
await ssb.sqlAsync(
`
SELECT
author,
(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
FROM messages
JOIN json_each(?) AS ids ON author = ids.value
GROUP BY author
`,
[JSON.stringify(ids)],
function (row) {
sizes[row.author] = row.size;
});
return sizes;
}
function niceSize(bytes) {
let value = bytes;
let unit = 'B';
@ -133,27 +240,28 @@ function niceSize(bytes) {
return Math.round(value * 10) / 10 + ' ' + unit;
}
async function buildTree(db, root, indent, depth) {
var f = await following(db, root);
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
if (depth > 0) {
for (let next of f) {
result += await buildTree(db, next, indent + ' ', depth - 1);
}
}
return result;
function escape(value) {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
}
async function main() {
await app.setDocument('<pre style="color: #fff">building...</pre>');
var db = await database('ssb');
var whoami = await ssb.getIdentities();
var tree = '';
for (let id of whoami) {
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
tree += await buildTree(db, id, '', 2);
let db = await database('ssb');
let whoami = await ssb.getIdentities();
let tree = '';
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
let following = await following_deep(whoami, 2, {});
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
let [about, sizes] = await Promise.all([
fetch_about(db, following, {}),
getSizes(following),
]);
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
for (let id of following) {
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
}
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
}
main();

4
apps/gg.json Normal file
View File

@ -0,0 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "🗺"
}

80
apps/gg/app.js Normal file
View File

@ -0,0 +1,80 @@
import * as tfrpc from '/tfrpc.js';
import * as strava from './strava.js';
let g_database;
let g_shared_database;
tfrpc.register(async function createIdentity() {
return ssb.createIdentity();
});
tfrpc.register(async function appendMessage(id, message) {
print('APPEND', JSON.stringify(message));
return ssb.appendMessageWithIdentity(id, message);
});
tfrpc.register(function url() {
return core.url;
});
tfrpc.register(async function getUser() {
return core.user;
});
tfrpc.register(function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function databaseGet(key) {
return g_database ? g_database.get(key) : undefined;
});
tfrpc.register(async function databaseSet(key, value) {
return g_database ? g_database.set(key, value) : undefined;
});
tfrpc.register(async function databaseRemove(key, value) {
return g_database ? g_database.remove(key, value) : undefined;
});
tfrpc.register(async function sharedDatabaseGet(key) {
return g_shared_database ? g_shared_database.get(key) : undefined;
});
tfrpc.register(async function sharedDatabaseSet(key, value) {
return g_shared_database ? g_shared_database.set(key, value) : undefined;
});
tfrpc.register(async function sharedDatabaseRemove(key, value) {
return g_shared_database ? g_shared_database.remove(key, value) : undefined;
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
});
tfrpc.register(async function store_blob(blob) {
if (typeof(blob) == 'string') {
blob = utf8Encode(blob);
}
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
}
return await ssb.blobStore(blob);
});
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
tfrpc.register(strava.refresh_token);
async function main() {
g_shared_database = await shared_database('state');
if (core.user.credentials?.session?.name) {
g_database = await database('state');
}
let attempt;
if (core.user.credentials?.session?.name) {
let shared_db = await shared_database('state');
attempt = await shared_db.get(core.user.credentials.session.name);
}
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
attempt: attempt,
state: core.user?.credentials?.session?.name,
})));
}
main();

81
apps/gg/gpx.js Normal file
View File

@ -0,0 +1,81 @@
function xml_parse(xml) {
let result;
let path = [];
let tag_begin;
let text_begin;
for (let i = 0; i < xml.length; i++) {
let c = xml.charAt(i);
if (!tag_begin && c == '<') {
if (i > text_begin && path.length) {
let value = xml.substring(text_begin, i);
if (!/^\s*$/.test(value)) {
path[path.length - 1].value = value;
}
}
tag_begin = i + 1;
} else if (tag_begin && c == '>') {
let tag = xml.substring(tag_begin, i).trim();
if (tag.startsWith('?') && tag.endsWith('?')) {
/* Ignore directives. */
} else if (tag.startsWith('/')) {
path.pop();
} else {
let parts = tag.split(' ');
let attributes = {};
for (let j = 1; j < parts.length; j++) {
let eq = parts[j].indexOf('=');
let value = parts[j].substring(eq + 1);
if (value.startsWith('"') && value.endsWith('"')) {
value = value.substring(1, value.length - 1);
}
attributes[parts[j].substring(0, eq)] = value;
}
let next = {name: parts[0], children: [], attributes: attributes};
if (path.length) {
path[path.length - 1].children.push(next);
} else {
result = next;
}
if (!tag.endsWith('/')) {
path.push(next);
}
}
tag_begin = undefined;
text_begin = i + 1;
}
}
return result;
}
function* xml_each(node, name) {
for (let child of node.children) {
if (child.name == name) {
yield child;
}
}
}
export function gpx_parse(xml) {
let result = {segments: []};
let tree = xml_parse(xml);
if (tree?.name == 'gpx') {
for (let trk of xml_each(tree, 'trk')) {
for (let trkseg of xml_each(trk, 'trkseg')) {
let segment = [];
for (let trkpt of xml_each(trkseg, 'trkpt')) {
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
}
result.segments.push(segment);
}
}
}
for (let metadata of xml_each(tree, 'metadata')) {
for (let link of xml_each(metadata, 'link')) {
result.link = link.attributes.href;
}
for (let time of xml_each(metadata, 'time')) {
result.time = time.value;
}
}
return result;
}

21
apps/gg/handler.js Normal file
View File

@ -0,0 +1,21 @@
import * as strava from './strava.js';
async function main() {
print('handler running');
let r = await strava.authorization_code(request.query.code);
print('state =', request.query.state);
print('body = ', r.body);
if (request.query.state && r.body) {
let shared_db = await shared_database('state');
await shared_db.set(request.query.state, utf8Decode(r.body));
}
await respond({
data: r.body,
content_type: 'text/plain',
headers: {
Location: 'https://tildefriends.net/~cory/gg/',
},
status_code: 307,
});
}
main();

14
apps/gg/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
<head>
<script>window.litDisableBundleWarning = true;</script>
<script>
let g_data = ${data};
</script>
<script src="script.js" type="module"></script>
<script src="leaflet.js"></script>
</head>
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
</body>
</html>

661
apps/gg/leaflet.css Normal file
View File

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
apps/gg/leaflet.js Normal file

File diff suppressed because one or more lines are too long

1
apps/gg/leaflet.js.map Normal file

File diff suppressed because one or more lines are too long

126
apps/gg/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

158
apps/gg/polyline.js Normal file
View File

@ -0,0 +1,158 @@
/**
* Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
*
* Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js)
* by [Mark McClure](http://facstaff.unca.edu/mcmcclur/)
*
* @module polyline
*/
var polyline = {};
function py2_round(value) {
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
}
function encode(current, previous, factor) {
current = py2_round(current * factor);
previous = py2_round(previous * factor);
var coordinate = (current - previous) * 2;
if (coordinate < 0) {
coordinate = -coordinate - 1
}
var output = '';
while (coordinate >= 0x20) {
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
coordinate /= 32;
}
output += String.fromCharCode((coordinate | 0) + 63);
return output;
}
/**
* Decodes to a [latitude, longitude] coordinates array.
*
* This is adapted from the implementation in Project-OSRM.
*
* @param {String} str
* @param {Number} precision
* @returns {Array}
*
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
*/
polyline.decode = function(str, precision) {
var index = 0,
lat = 0,
lng = 0,
coordinates = [],
shift = 0,
result = 0,
byte = null,
latitude_change,
longitude_change,
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
// Coordinates have variable length when encoded, so just keep
// track of whether we've hit the end of the string. In each
// loop iteration, a single coordinate is decoded.
while (index < str.length) {
// Reset shift, result, and byte
byte = null;
shift = 1;
result = 0;
do {
byte = str.charCodeAt(index++) - 63;
result += (byte & 0x1f) * shift;
shift *= 32;
} while (byte >= 0x20);
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
shift = 1;
result = 0;
do {
byte = str.charCodeAt(index++) - 63;
result += (byte & 0x1f) * shift;
shift *= 32;
} while (byte >= 0x20);
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
lat += latitude_change;
lng += longitude_change;
coordinates.push([lat / factor, lng / factor]);
}
return coordinates;
};
/**
* Encodes the given [latitude, longitude] coordinates array.
*
* @param {Array.<Array.<Number>>} coordinates
* @param {Number} precision
* @returns {String}
*/
polyline.encode = function(coordinates, precision) {
if (!coordinates.length) { return ''; }
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
for (var i = 1; i < coordinates.length; i++) {
var a = coordinates[i], b = coordinates[i - 1];
output += encode(a[0], b[0], factor);
output += encode(a[1], b[1], factor);
}
return output;
};
function flipped(coords) {
var flipped = [];
for (var i = 0; i < coords.length; i++) {
var coord = coords[i].slice();
flipped.push([coord[1], coord[0]]);
}
return flipped;
}
/**
* Encodes a GeoJSON LineString feature/geometry.
*
* @param {Object} geojson
* @param {Number} precision
* @returns {String}
*/
polyline.fromGeoJSON = function(geojson, precision) {
if (geojson && geojson.type === 'Feature') {
geojson = geojson.geometry;
}
if (!geojson || geojson.type !== 'LineString') {
throw new Error('Input must be a GeoJSON LineString');
}
return polyline.encode(flipped(geojson.coordinates), precision);
};
/**
* Decodes to a GeoJSON LineString geometry.
*
* @param {String} str
* @param {Number} precision
* @returns {Object}
*/
polyline.toGeoJSON = function(str, precision) {
var coords = polyline.decode(str, precision);
return {
type: 'LineString',
coordinates: flipped(coords)
};
};
let polyline_decode = polyline.decode;
export { polyline_decode as decode };

789
apps/gg/script.js Normal file
View File

@ -0,0 +1,789 @@
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as polyline from './polyline.js';
import {gpx_parse} from './gpx.js';
const k_client_id = '28276';
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
const k_color_snow = [128, 128, 255, 255];
const k_color_ice = [160, 160, 255, 255];
const k_color_water = [0, 0, 255, 255];
const k_color_dirt = [128, 129, 130, 255];
const k_color_pavement = [32, 32, 32, 255];
const k_color_grass = [0, 255, 0, 255];
const k_color_default = [128, 128, 128, 255];
const k_store = {
'🦞': 15,
'🛶': 10,
'🏠': 10,
'⛰': 10,
};
const k_marker_snap = {x: 5, y: 1};
class GgAppElement extends LitElement {
static get properties() {
return {
user: {type: Object},
strava: {type: Object},
activities: {type: Array},
activity: {type: Object},
world: {type: Object},
whoami: {type: String},
status: {type: Object},
tab: {type: String},
url: {type: String},
currency: {type: Number},
to_build: {type: String},
};
}
constructor() {
super();
this.activities = [];
this.activity = {};
this.loaded_activities = [];
this.placed_emojis = [];
this.strava = {};
this.min_lat = Number.MAX_VALUE;
this.min_lon = Number.MAX_VALUE;
this.max_lat = -Number.MAX_VALUE;
this.max_lon = -Number.MAX_VALUE;
this.focus = undefined;
this.status = undefined;
this.tab = 'map';
this.load().catch(function(e) {
console.log('load error', e);
});
this.to_build = '🏠';
}
async load() {
console.log('load');
this.user = await tfrpc.rpc.getUser();
this.url = (await tfrpc.rpc.url()).split('?')[0];
try {
await this.update_credentials();
} catch (e) {
console.log('update_credentials failed', e);
}
try {
await this.update_activities();
} catch (e) {
console.log('update_activities failed', e);
}
await this.acquire_ssb_identity();
if (this.whoami && this.activities?.length) {
await this.sync_activities();
}
await this.get_activities_from_ssb();
}
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
async promise_all(promises, max_concurrent) {
let index = 0;
let results = [];
async function exec_thread() {
while (index < promises.length) {
const current = index++;
results[current] = await promises[current];
}
}
const threads = [];
for (let thread = 0; thread < max_concurrent; thread++) {
threads.push(exec_thread());
}
await Promise.all(threads);
return results;
}
async get_activities_from_ssb() {
this.status = {text: 'loading activities'};
this.loaded_activities = [];
let rows = await tfrpc.rpc.query(`
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
FROM messages_fts('"gg-activity"')
JOIN messages ON messages.rowid = messages_fts.rowid,
json_each(messages.content, '$.mentions') as mention
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
json_extract(mention.value, '$.name') = 'activity_data'
ORDER BY messages.timestamp DESC
`, []);
this.status = {text: 'loading activity data'};
let authors = rows.map(x => x.author);
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
this.status = {text: 'processing activity data'};
for (let [index, blob] of blobs.entries()) {
let activity;
try {
activity = JSON.parse(blob);
} catch {
activity = gpx_parse(blob);
}
if (activity) {
activity.author = authors[index];
this.loaded_activities.push(activity);
}
}
this.status = {text: 'calculating balance'};
rows = await tfrpc.rpc.query(`
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
`, [this.whoami]);
let currency = rows[0].currency;
rows = await tfrpc.rpc.query(`
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
`, [this.whoami]);
let spent = rows[0].cost;
this.currency = currency - spent;
this.status = {text: 'getting placed emojis'};
rows = await tfrpc.rpc.query(`
SELECT messages.content
FROM messages_fts('"gg-place"')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'gg-place'
ORDER BY messages.timestamp
`);
for (let row of rows) {
console.log(row.content);
let content = JSON.parse(row.content);
this.placed_emojis.push({
position: content.position,
emoji: content.emoji,
});
}
console.log(this.placed_emojis);
this.status = undefined;
this.update_map();
}
async sync_activities() {
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
let missing = await tfrpc.rpc.query(`
WITH my_activities AS (
SELECT json_extract(mention.value, '$.link') AS url
FROM messages, json_each(messages.content, '$.mentions') AS mention
WHERE
author = ? AND
json_extract(messages.content, '$.type') = 'gg-activity' AND
json_extract(mention.value, '$.name') = 'activity_url')
SELECT from_strava.value FROM json_each(?) AS from_strava
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
WHERE my_activities.url IS NULL
`, [this.whoami, JSON.stringify(ids)]);
console.log('missing = ', missing);
for (let [index, row] of missing.entries()) {
this.status = {text: 'syncing from strava', value: index, max: missing.length};
let url = row.value;
let id = url.match(/.*\/(\d+)/)[1];
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
headers: {
'Authorization': `Bearer ${this.strava.access_token}`,
},
});
let activity = await response.json();
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
let message = {
type: 'gg-activity',
mentions: [
{
link: url,
name: 'activity_url',
},
{
link: blob_id,
name: 'activity_data',
}
],
};
await tfrpc.rpc.appendMessage(this.whoami, message);
}
this.status = undefined;
}
async acquire_ssb_identity() {
let user = await tfrpc.rpc.getUser();
if (!user?.credentials?.session?.name) {
return;
}
let ids = await tfrpc.rpc.getIdentities();
let players = ids.length ? (await tfrpc.rpc.query(`
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
WHERE
json_extract(messages.content, '$.type') = 'gg-player' AND
json_extract(messages.content, '$.active')
ORDER BY timestamp DESC limit 1
`, [JSON.stringify(ids)])).map(row => row.author) : [];
if (!players.length) {
this.whoami = await tfrpc.rpc.createIdentity();
if (this.whoami) {
await tfrpc.rpc.appendMessage(this.whoami, {
type: 'gg-player',
active: true,
});
}
} else {
players.sort();
this.whoami = players[0];
}
}
async update_credentials() {
let name = this.user?.credentials?.session?.name;
if (!name) {
return;
}
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
if (shared) {
await tfrpc.rpc.databaseSet('strava', shared);
await tfrpc.rpc.sharedDatabaseRemove(name);
}
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
let x = await tfrpc.rpc.refresh_token(this.strava);
if (x) {
this.strava = x;
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
} else {
this.strava = null;
}
}
}
async update_activities() {
if (this?.strava?.access_token) {
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
headers: {
'Authorization': `Bearer ${this.strava.access_token}`,
},
});
this.activities = await response.json();
this.activities.sort((a, b) => (a.id - b.id));
}
}
color_to_emoji(color) {
const k_map = [
[k_color_snow, '⬜'],
[k_color_ice, '🟦'],
[k_color_water, '🟦'],
[k_color_dirt, '🟫'],
[k_color_pavement, '⬛'],
[k_color_grass, '🟩'],
[k_color_default, '🟧'],
];
for (let m of k_map) {
if (m[0][0] == color[0] &&
m[0][1] == color[1] &&
m[0][2] == color[2] &&
m[0][3] == color[3]) {
return m[1];
}
}
}
activity_bounds(activity) {
let min_lat = Number.MAX_VALUE;
let min_lon = Number.MAX_VALUE;
let max_lat = -Number.MAX_VALUE;
let max_lon = -Number.MAX_VALUE;
if (activity?.map?.polyline) {
for (let pt of polyline.decode(activity.map.polyline)) {
min_lat = Math.min(min_lat, pt[0]);
min_lon = Math.min(min_lon, pt[1]);
max_lat = Math.max(max_lat, pt[0]);
max_lon = Math.max(max_lon, pt[1]);
}
}
if (activity?.segments) {
for (let segment of activity.segments) {
for (let pt of segment) {
min_lat = Math.min(min_lat, pt.lat);
min_lon = Math.min(min_lon, pt.lon);
max_lat = Math.max(max_lat, pt.lat);
max_lon = Math.max(max_lon, pt.lon);
}
}
}
return {
min: {
lat: min_lat,
lng: min_lon,
},
max: {
lat: max_lat,
lng: max_lon,
},
};
}
on_click(event) {
let popup = L.popup()
.setLatLng(event.latlng)
.setContent(`
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
`)
.openOn(this.leaflet);
}
async build() {
if (this.popup) {
this.popup.remove();
}
if (!this.marker) {
return;
}
let latlng = this.marker.getLatLng();
let cost = k_store[this.to_build];
if (cost > this.currency) {
alert('Insufficient funds.');
return;
}
let message = {
type: 'gg-place',
position: {lat: latlng.lat, lng: latlng.lng},
emoji: this.to_build,
cost: cost,
};
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
this.marker.remove();
this.placed_emojis.push({
position: {lat: latlng.lat, lng: latlng.lng},
emoji: this.to_build,
});
this.currency -= cost;
return this.update_map();
}
on_marker_click(event) {
this.popup = L.popup()
.setLatLng(event.latlng)
.setContent(`
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
`)
.openOn(this.leaflet);
}
snap_to_grid(latlng, fudge) {
let position = this.leaflet.options.crs.latLngToPoint(latlng, this.leaflet.getZoom());
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
position = this.leaflet.options.crs.pointToLatLng(position, this.leaflet.getZoom());
return position;
}
on_marker_move(event) {
if (!this.no_snap && this.marker) {
this.no_snap = true;
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
this.no_snap = false;
}
}
on_mouse_down(event) {
if (this.marker) {
this.marker.remove();
this.marker = undefined;
}
if (this.to_build) {
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
this.marker.on({click: this.on_marker_click.bind(this)});
this.marker.on({drag: this.on_marker_move.bind(this)});
}
}
async update_map() {
let map = this.shadowRoot.getElementById('map');
if (!map || !this.loaded_activities.length) {
this.leaflet = undefined;
this.grid_layer = undefined;
return;
}
if (!this.leaflet) {
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
this.leaflet.on({contextmenu: this.on_click.bind(this)});
this.leaflet.on({click: this.on_mouse_down.bind(this)});
}
let self = this;
let grid_layer = L.GridLayer.extend({
createTile: function(coords) {
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
var size = this.getTileSize();
tile.width = size.x;
tile.height = size.y;
var context = tile.getContext('2d');
context.font = '10pt sans';
let bounds = this._tileCoordsToBounds(coords);
let degrees = 360.0 / (2 ** coords.z);
let ul = bounds.getNorthWest();
let lr = bounds.getSouthEast();
let mini = document.createElement('canvas');
mini.width = Math.floor(size.x / 16.0);
mini.height = Math.floor(size.y / 16.0);
let mini_context = mini.getContext('2d');
let image_data = context.getImageData(0, 0, mini.width, mini.height);
for (let activity of self.loaded_activities) {
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
}
context.textAlign = 'left';
context.textBaseline = 'top';
for (let x = 0; x < mini.width; x++) {
for (let y = 0; y < mini.height; y++) {
let start = (y * mini.width + x) * 4;
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
if (pixel) {
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height);
}
}
}
for (let placed of self.placed_emojis) {
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position), coords.z);
let tile_x = Math.floor(position.x / size.x);
let tile_y = Math.floor(position.y / size.y);
position.x = position.x - tile_x * size.x;
position.y = position.y - tile_y * size.y;
if (tile_x == coords.x && tile_y == coords.y) {
context.fillText(placed.emoji, position.x, position.y);
}
}
return tile;
}
});
if (this.grid_layer) {
this.grid_layer.redraw();
} else {
this.grid_layer = new grid_layer();
this.grid_layer.addTo(this.leaflet);
}
for (let activity of this.loaded_activities) {
let bounds = this.activity_bounds(activity);
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
}
if (this.focus) {
this.leaflet.fitBounds([
this.focus.min,
this.focus.max,
]);
this.focus = undefined;
} else {
this.leaflet.fitBounds([
[this.min_lat, this.min_lon],
[this.max_lat, this.max_lon],
]);
}
}
activity_to_color(activity) {
let color = [0, 0, 0, 255];
switch (activity.sport_type) {
/* Implies snow. */
case 'AlpineSki':
case 'BackcountrySki':
case 'NordicSki':
case 'Snowshoe':
case 'Snowboard':
color = k_color_snow;
break;
/* Implies ice. */
case 'IceSkate':
case 'InlineSkate':
color = k_color_ice;
break;
/* Implies water. */
case 'Canoeing':
case 'Kayaking':
case 'Kitesurf':
case 'Rowing':
case 'Sail':
case 'StandUpPaddling':
case 'Surfing':
case 'Swim':
case 'Windsurf':
color = k_color_water;
break;
/* Implies dirt. */
case 'EMountainBikeRide':
case 'Hike':
case 'MountainBikeRide':
case 'RockClimbing':
case 'TrailRun':
color = k_color_dirt;
break;
/* Implies pavement. */
case 'EBikeRide':
case 'GravelRide':
case 'Handcycle':
case 'Ride':
case 'RollerSki':
case 'Run':
case 'Skateboard':
case 'Badminton':
case 'Tennis':
case 'Velomobile':
case 'Walk':
case 'Wheelchair':
color = k_color_pavement;
break;
/* Grass, maybe? */
case 'Golf':
case 'Soccer':
case 'Squash':
color = k_color_grass;
break;
// Crossfit,
// Elliptical
// HighIntensityIntervalTraining
// Pickleball
// Pilates
// Racquetball
// StairStepper
// TableTennis,
// VirtualRide
// VirtualRow
// VirtualRun
// WeightTraining
// Workout
// Yoga
default:
color = k_color_default;
}
return color;
}
line(image_data, x0, y0, x1, y1, value) {
/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
let dx = Math.abs(x1 - x0);
let sx = x0 < x1 ? 1 : -1;
let dy = -Math.abs(y1 - y0);
let sy = y0 < y1 ? 1 : -1;
let error = dx + dy;
while (true) {
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
let base = (y0 * image_data.width + x0) * 4;
image_data.data[base + 0] = value[0];
image_data.data[base + 1] = value[1];
image_data.data[base + 2] = value[2];
image_data.data[base + 3] = value[3];
}
if (x0 == x1 && y0 == y1) {
break;
}
let e2 = 2 * error;
if (e2 >= dy) {
if (x0 == x1) {
break;
}
error += dy;
x0 = Math.round(x0 + sx);
}
if (e2 <= dx) {
if (y0 == y1) {
break;
}
error += dx;
y0 = Math.round(y0 + sy);
}
}
}
draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
let color = this.activity_to_color(activity);
if (activity?.map?.polyline) {
let last;
for (let pt of polyline.decode(activity.map.polyline)) {
let px = [
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
];
if (last) {
this.line(image_data, last[0], last[1], px[0], px[1], color);
}
last = px;
}
}
if (activity?.segments) {
for (let segment of activity.segments) {
let last;
for (let pt of segment) {
let px = [
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
];
if (last) {
this.line(image_data, last[0], last[1], px[0], px[1], color);
}
last = px;
}
}
}
}
async on_upload(event) {
try {
let file = event.srcElement.files[0];
let xml = await file.text();
let gpx = gpx_parse(xml);
let blob_id = await tfrpc.rpc.store_blob(xml);
console.log('blob_id = ', blob_id);
console.log(gpx);
let message = {
type: 'gg-activity',
mentions: [
{
link: `https://${gpx.link}/activity/${gpx.time}`,
name: 'activity_url',
},
{
link: blob_id,
name: 'activity_data',
}
],
};
console.log('id =', this.whoami, 'message = ', message);
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
console.log('appended message', id);
alert('Activity uploaded.');
await this.get_activities_from_ssb();
} catch (e) {
alert(`Error: ${JSON.stringify(e, null, 2)}`);
}
}
upload() {
let input = document.createElement('input');
input.type = 'file';
input.onchange = (event) => this.on_upload(event);
input.click();
}
updated() {
this.update_map();
}
focus_map(activity) {
let bounds = this.activity_bounds(activity);
if (bounds.min.lat < bounds.max.lat &&
bounds.min.lng < bounds.max.lng) {
this.tab = 'map';
this.focus = bounds;
}
}
render_news() {
return html`
<ul>
${this.loaded_activities.map(x => html`
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
`)}
</ul>
`;
}
render_store_item(item) {
let [emoji, cost] = item;
return html`
<div>
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
</div>
`;
}
render_store() {
return html`
<h2>Store</h2>
<div><b>Your balance:</b> ${this.currency}</div>
${Object.entries(k_store).map(this.render_store_item.bind(this))}
`;
}
render() {
let header;
if (!this.user?.credentials?.session?.name) {
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
} else if (!this.strava?.access_token) {
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
header = html`
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
<input type="button" value="📁" @click=${this.upload}></input>
</div>
`;
} else {
header = html`
<div>
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
<h1>Welcome, ${this.user.credentials.session.name}</h1>
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
<input type="button" value="📁" @click=${this.upload}></input>
</div>
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
</div>
`;
}
let navigation = html`
<style>
#navigation input[type="button"] {
min-width: 3em;
min-height: 3em;
flex: 1 0;
font-size: large;
}
</style>
<div id="navigation" style="display: flex; flex-direction: row">
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺Map"></input>
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗Store"></input>
</div>
`;
let content;
switch (this.tab) {
case 'map':
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
break;
case 'news':
content = this.render_news();
break;
case 'friends':
content = html`<div>Friends</div>`;
break;
case 'store':
content = this.render_store();
break;
}
return html`
<style>
.build-icon::before {
content: '📍';
border: 2px solid red;
}
</style>
<link rel="stylesheet" href="leaflet.css"/>
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
${header}
<div style="flex: 1 0; overflow: scroll">${content}</div>
${navigation}
</div>
`;
}
}
customElements.define('gg-app', GgAppElement);

20
apps/gg/strava.js Normal file
View File

@ -0,0 +1,20 @@
const k_client_id = '28276';
const k_client_secret = '3123f1f5afe132d9731111066d1d17bdb22ef27e';
const k_access_token = 'f753e77764c26252bd2d80e7c5cc17ace51a8864';
const k_refresh_token = 'f58d8e1b5a3ec3bf96e681589d5014f9a294f5a4';
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
export async function refresh_token(token) {
let r = await fetch('https://www.strava.com/api/v3/oauth/token', {
method: 'POST',
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&refresh_token=${token.refresh_token}&grant_type=refresh_token`,
});
return r?.body ? JSON.parse(utf8Decode(r.body)) : undefined;
}
export async function authorization_code(code) {
return await fetch('https://www.strava.com/api/v3/oauth/token', {
method: 'POST',
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`,
});
}

4
apps/issues.json Normal file
View File

@ -0,0 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "🦟"
}

105
apps/issues/app.js Normal file
View File

@ -0,0 +1,105 @@
import * as tfrpc from '/tfrpc.js';
let g_database;
let g_hash;
tfrpc.register(async function localStorageGet(key) {
return app.localStorageGet(key);
});
tfrpc.register(async function localStorageSet(key, value) {
return app.localStorageSet(key, value);
});
tfrpc.register(async function databaseGet(key) {
return g_database ? g_database.get(key) : undefined;
});
tfrpc.register(async function databaseSet(key, value) {
return g_database ? g_database.set(key, value) : undefined;
});
tfrpc.register(async function createIdentity() {
return ssb.createIdentity();
});
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function getAllIdentities() {
return ssb.getAllIdentities();
});
tfrpc.register(async function getBroadcasts() {
return ssb.getBroadcasts();
});
tfrpc.register(async function getConnections() {
return ssb.connections();
});
tfrpc.register(async function getStoredConnections() {
return ssb.storedConnections();
});
tfrpc.register(async function forgetStoredConnection(connection) {
return ssb.forgetStoredConnection(connection);
});
tfrpc.register(async function createTunnel(portal, target) {
return ssb.createTunnel(portal, target);
});
tfrpc.register(async function connect(token) {
await ssb.connect(token);
});
tfrpc.register(async function closeConnection(id) {
await ssb.closeConnection(id);
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
});
tfrpc.register(async function appendMessage(id, message) {
return ssb.appendMessageWithIdentity(id, message);
});
core.register('message', async function message_handler(message) {
if (message.event == 'hashChange') {
g_hash = message.hash;
await tfrpc.rpc.hashChanged(message.hash);
}
});
tfrpc.register(function getHash(id, message) {
return g_hash;
});
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);
}
return await ssb.blobStore(blob);
});
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
tfrpc.register(async function store_message(message) {
return await ssb.storeMessage(message);
});
tfrpc.register(function apps() {
return core.apps();
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
ssb.addEventListener('broadcasts', async function() {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function() {
await tfrpc.rpc.set('connections', await ssb.connections());
});
async function main() {
if (typeof(database) !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();

View File

@ -0,0 +1,91 @@
function textNode(text) {
const node = new commonmark.Node("text", undefined);
node.literal = text;
return node;
}
function linkNode(text, url) {
const urlNode = new commonmark.Node("link", undefined);
urlNode.destination = url;
urlNode.appendChild(textNode(text));
return urlNode;
}
function splitMatches(text, regexp) {
// Regexp must be sticky.
regexp = new RegExp(regexp, "gm");
let i = 0;
const result = [];
let match = regexp.exec(text);
while (match) {
const matchText = match[0];
if (match.index > i) {
result.push([text.substring(i, match.index), false]);
}
result.push([matchText, true]);
i = match.index + matchText.length;
match = regexp.exec(text);
}
if (i < text.length) {
result.push([text.substring(i, text.length), false]);
}
return result;
}
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
function splitURLs(textNodes) {
const text = textNodes.map(n => n.literal).join("");
const parts = splitMatches(text, urlRegexp);
return parts.map(part => {
if (part[1]) {
return linkNode(part[0], part[0]);
} else {
return textNode(part[0]);
}
});
}
export function transform(parsed) {
const walker = parsed.walker();
let event;
let nodes = [];
while ((event = walker.next())) {
const node = event.node;
if (event.entering && node.type === "text") {
nodes.push(node);
} else {
if (nodes.length > 0) {
splitURLs(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
nodes = [];
}
}
}
if (nodes.length > 0) {
splitURLs(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
}
return parsed;
}

1
apps/issues/commonmark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14
apps/issues/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top">
</head>
<body>
<tf-issues-app/>
<script>window.litDisableBundleWarning = true;</script>
<script src="commonmark.min.js"></script>
<script src="commonmark-linkify.js" type="module"></script>
<script src="script.js" type="module"></script>
</body>
</html>

120
apps/issues/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

260
apps/issues/script.js Normal file
View File

@ -0,0 +1,260 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
class TfIdPickerElement extends LitElement {
static get properties() {
return {
ids: {type: Array},
selected: {type: String},
};
}
constructor() {
super();
this.load();
}
async load() {
this.selected = await tfrpc.rpc.localStorageGet('whoami');
this.ids = (await tfrpc.rpc.getIdentities()) || [];
}
changed(event) {
this.selected = event.srcElement.value;
tfrpc.rpc.localStorageSet('whoami', this.selected);
}
render() {
if (this.ids) {
return html`
<select @change=${this.changed} style="max-width: 100%">
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
</select>
`;
} else {
return html`<div>Loading...</div>`;
}
}
}
customElements.define('tf-id-picker', TfIdPickerElement);
class TfComposeElement extends LitElement {
static get properties() {
return {
value: {type: String},
};
}
input() {
let input = this.renderRoot.getElementById('input');
let preview = this.renderRoot.getElementById('preview');
if (input && preview) {
preview.innerHTML = tfutils.markdown(input.value);
}
}
submit() {
this.dispatchEvent(new CustomEvent('tf-submit', {
bubbles: true,
composed: true,
detail: {
value: this.renderRoot.getElementById('input').value,
},
}));
this.renderRoot.getElementById('input').value = '';
this.input();
}
render() {
return html`
<div style="display: flex; flex-direction: row">
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
<div id="preview" style="flex: 1 1"></div>
</div>
<input type="submit" value="Submit" @click=${this.submit}></input>
`;
}
}
customElements.define('tf-compose', TfComposeElement);
class TfIssuesAppElement extends LitElement {
static get properties() {
return {
issues: {type: Array},
selected: {type: Object},
};
}
constructor() {
super();
this.issues = [];
this.load();
}
async load() {
let issues = {};
let messages = await tfrpc.rpc.query(`
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
issues.id = messages_refs.ref JOIN messages ON
messages.id = messages_refs.message
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
SELECT * FROM issues
UNION
SELECT * FROM edits ORDER BY timestamp
`, [k_project]);
for (let message of messages) {
let content = JSON.parse(message.content);
switch (content.type) {
case 'issue':
issues[message.id] = {
id: message.id,
author: message.author,
text: content.text,
updates: [],
created: message.timestamp,
open: true,
};
break;
case 'issue-edit':
case 'post':
for (let issue of (content.issues || [])) {
if (issues[issue.link]) {
if (issue.open !== undefined) {
issues[issue.link].open = issue.open;
message.open = issue.open;
}
issues[issue.link].updates.push(message);
issues[issue.link].updated = message.timestamp;
}
}
break;
}
}
this.issues = Object.values(issues).sort((x, y) => y.created - x.created);
if (this.selected) {
for (let issue of this.issues) {
if (issue.id == this.selected.id) {
this.selected = issue;
}
}
}
}
render_issue_table_row(issue) {
return html`
<tr>
<td>${issue.open ? 'open' : 'closed'}</td>
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
${issue.text.split('\n')?.[0]}
</td>
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
</tr>
`;
}
render_update(update) {
let content = JSON.parse(update.content);
let message;
if (content.text) {
message = unsafeHTML(tfutils.markdown(content.text));
}
return html`
<div style="border-left: 2px solid #fff; padding-left: 8px">
<div>${new Date(update.timestamp).toLocaleString()}</div>
<div>${update.author}</div>
<div>${message}</div>
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
</div>
`;
}
async set_open(id, open) {
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit',
issues: [
{
link: id,
open: open,
},
],
});
await this.load();
}
}
async create_issue(event) {
let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue',
project: k_project,
text: event.detail.value,
});
await this.load();
}
async reply_to_issue(event) {
let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, {
type: 'post',
text: event.detail.value,
root: this.selected.id,
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
issues: [
{
link: this.selected.id,
},
],
});
await this.load();
}
render() {
let header = html`
<h1>Tilde Friends Issues</h1>
<tf-id-picker id="picker"></tf-id-picker>
`;
if (this.selected) {
return html`
${header}
<div>
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
${this.selected.open ?
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
</div>
<div>${new Date(this.selected.created).toLocaleString()}</div>
<div>${this.selected.author}</div>
<div>${this.selected.id}</div>
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
${this.selected.updates.map(x => this.render_update(x))}
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
`;
} else {
return html`
${header}
<h2>New Issue</h2>
<tf-compose @tf-submit=${this.create_issue}></tf-compose>
<table>
<tr>
<th>Status</th>
<th>Author</th>
<th>Title</th>
<th>Date</th>
</tr>
${this.issues.map(x => this.render_issue_table_row(x))}
</table>
`;
}
}
}
customElements.define('tf-issues-app', TfIssuesAppElement);

91
apps/issues/tf-utils.js Normal file
View File

@ -0,0 +1,91 @@
import * as linkify from './commonmark-linkify.js';
function image(node, entering) {
if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')) {
if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')) {
if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
writer.image = image;
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
var walker = parsed.walker();
var event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}
export function human_readable_size(bytes) {
let v = bytes;
let u = 'B';
for (let unit of ['kB', 'MB', 'GB']) {
if (v > 1024) {
v /= 1024;
u = unit;
} else {
break;
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}

4
apps/sneaker.json Normal file
View File

@ -0,0 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "👟"
}

30
apps/sneaker/app.js Normal file
View File

@ -0,0 +1,30 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(async function getAllIdentities() {
return ssb.getAllIdentities();
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
}
return await ssb.blobStore(blob);
});
tfrpc.register(async function get_blob(id) {
return Array.from(new Uint8Array(await ssb.blobGet(id)));
});
tfrpc.register(async function store_message(message) {
return await ssb.storeMessage(message);
});
async function main() {
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();

3
apps/sneaker/filesaver.min.js vendored Normal file
View File

@ -0,0 +1,3 @@
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
//# sourceMappingURL=FileSaver.min.js.map

14
apps/sneaker/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top">
</head>
<body>
<tf-sneaker-app/>
<script>window.litDisableBundleWarning = true;</script>
<script src="filesaver.min.js"></script>
<script src="jszip.min.js"></script>
<script src="script.js" type="module"></script>
</body>
</html>

13
apps/sneaker/jszip.min.js vendored Normal file

File diff suppressed because one or more lines are too long

120
apps/sneaker/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

232
apps/sneaker/script.js Normal file
View File

@ -0,0 +1,232 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfSneakerAppElement extends LitElement {
static get properties() {
return {
feeds: {type: Object},
progress: {type: Object},
result: {type: String},
};
}
constructor() {
super();
this.feeds = [];
this.progress = undefined;
this.result = undefined;
}
async search() {
let q = this.renderRoot.getElementById('search').value;
let result = await tfrpc.rpc.query(`
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE
json_extract(messages.content, '$.type') = 'about' AND
json_extract(messages.content, '$.about') = messages.author AND
json_extract(messages.content, '$.name') IS NOT NULL
GROUP BY messages.author
HAVING MAX(messages.sequence)
ORDER BY COUNT(*) DESC
`,
[`"${q.replaceAll('"', '""')}"`]);
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
}
format_message(message) {
let out = {
previous: message.previous ?? null,
};
if (message.sequence_before_author) {
out.sequence = message.sequence;
out.author = message.author;
} else {
out.author = message.author;
out.sequence = message.sequence;
}
out.timestamp = message.timestamp;
out.hash = message.hash;
out.content = JSON.parse(message.content);
out.signature = message.signature;
return {key: message.id, value: out};
}
sanitize(value) {
return value.replaceAll('/', '_').replaceAll('+', '-');
}
guess_ext(data) {
function startsWith(prefix) {
if (data.length < prefix.length) {
return false;
}
for (let i = 0; i < prefix.length; i++) {
if (prefix[i] !== null && data[i] !== prefix[i]) {
return false;
}
}
return true;
}
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
return '.jpg';
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
return '.png';
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
return '.gif';
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
return '.webp';
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
return '.svg';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
return '.mp3';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
return '.mp4';
} else {
return '.bin';
}
}
async export(id) {
let all_messages = '';
let sequence = -1;
let messages_done = 0;
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
while (true) {
let messages = await tfrpc.rpc.query(
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
[id, sequence]
);
if (messages?.length) {
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
sequence = messages[messages.length - 1].sequence;
messages_done += messages.length;
this.progress = {name: 'messages', value: messages_done, max: messages_max};
} else {
break;
}
}
let zip = new JSZip();
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
let blobs = await tfrpc.rpc.query(
`SELECT messages_refs.ref AS id
FROM messages
JOIN messages_refs ON messages.id = messages_refs.message
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
[id]);
let blobs_done = 0;
for (let row of blobs) {
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
let blob;
try {
blob = await tfrpc.rpc.get_blob(row.id);
} catch (e) {
console.log(`Failed to get ${row.id}: ${e.message}`);
}
if (blob) {
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
}
blobs_done++;
}
this.progress = {name: 'saving'};
let blob = await zip.generateAsync({type: 'blob'});
saveAs(blob, `${this.sanitize(id)}.zip`);
this.progress = null;
}
keypress(event) {
if (event.key == 'Enter') {
this.search();
}
}
async import(event) {
let file = event.target.files[0];
if (!file) {
return;
}
this.progress = {name: 'loading'};
let zip = new JSZip();
file = await zip.loadAsync(file);
let messages = [];
let blobs = [];
file.forEach(function(path, entry) {
if (!entry.dir) {
if (path.startsWith('message/classic/')) {
messages.push(entry);
} else {
blobs.push(entry);
}
}
});
let success = {messages: 0, blobs: 0};
let progress = 0;
let total_messages = 0;
for (let entry of messages) {
let lines = (await entry.async('string')).split('\n');
total_messages += lines.length;
for (let line of lines) {
if (!line.length) {
continue;
}
let message = JSON.parse(line);
this.progress = {name: 'messages', value: progress++, max: total_messages};
if (await tfrpc.rpc.store_message(message.value)) {
success.messages++;
}
}
}
progress = 0;
for (let blob of blobs) {
this.progress = {name: 'blobs', value: progress++, max: blobs.length};
if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
success.blobs++;
}
}
this.progress = undefined;
this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
}
render() {
let progress;
if (this.progress) {
if (this.progress.max) {
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
} else {
progress = html`<div><span>${this.progress.name}</span></div>`;
}
}
return html`<h1>SSB 👟net</h1>
<code>${this.result}</code>
${progress}
<h2>Import</h2>
<input type="file" id="import" @change=${this.import}></input>
<h2>Export</h2>
<input type="text" id="search" @keypress=${this.keypress}></input>
<input type="button" value="Search Users" @click=${this.search}></input>
<ul>
${Object.entries(this.feeds).map(([id, name]) => html`
<li>
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
${name}
<code style="color: #ccc">${id}</code>
</li>
`)}
</ul>
`;
}
}
customElements.define('tf-sneaker-app', TfSneakerAppElement);

View File

@ -79,9 +79,18 @@ tfrpc.register(async function store_blob(blob) {
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
tfrpc.register(async function store_message(message) {
return await ssb.storeMessage(message);
});
tfrpc.register(function apps() {
return core.apps();
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
tfrpc.register(async function encrypt(id, recipients, content) {
return await ssb.privateMessageEncrypt(id, recipients, content);
});
ssb.addEventListener('broadcasts', async function() {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});

View File

@ -54,21 +54,27 @@ export function picker(callback, anchor) {
}
}
function chosen(event) {
console.log(event.srcElement.innerText);
callback(event.srcElement.innerText);
cleanup();
}
function refresh() {
while (list.firstChild) {
list.removeChild(list.firstChild);
}
let search = input.value;
let search = input.value.toLowerCase();
let any_at_all = false;
Object.entries(json).forEach(function(row) {
for (let row of Object.entries(json)) {
let header = document.createElement('div');
header.appendChild(document.createTextNode(row[0]));
list.appendChild(header);
let any = false;
for (let entry of row[1]) {
for (let entry of Object.entries(row[1])) {
if (search &&
search.length &&
entry.name.indexOf(search) == -1) {
entry[0].toLowerCase().indexOf(search) == -1) {
continue;
}
let emoji = document.createElement('span');
@ -76,12 +82,9 @@ export function picker(callback, anchor) {
emoji.style.display = 'inline-block';
emoji.style.overflow = 'hidden';
emoji.style.cursor = 'pointer';
emoji.onclick = function() {
callback(entry);
cleanup();
}
emoji.title = entry.name;
emoji.appendChild(document.createTextNode(entry.emoji));
emoji.onclick = chosen;
emoji.title = entry[0];
emoji.appendChild(document.createTextNode(entry[1]));
list.appendChild(emoji);
any = true;
any_at_all = true;
@ -89,7 +92,7 @@ export function picker(callback, anchor) {
if (!any) {
list.removeChild(header);
}
});
}
if (!any_at_all) {
list.appendChild(document.createTextNode('No matches found.'));
}

File diff suppressed because one or more lines are too long

3
apps/ssb/filesaver.min.js vendored Normal file
View File

@ -0,0 +1,3 @@
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
//# sourceMappingURL=FileSaver.min.js.map

View File

@ -13,6 +13,7 @@
<body>
<tf-app/>
<script>window.litDisableBundleWarning = true;</script>
<script src="filesaver.min.js"></script>
<script src="commonmark.min.js"></script>
<script src="commonmark-linkify.js" type="module"></script>
<script src="commonmark-hashtag.js" type="module"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,6 +8,10 @@ import * as tf_user from './tf-user.js';
import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.js';
import * as tf_tab_mentions from './tf-tab-mentions.js';
import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js';

View File

@ -16,6 +16,7 @@ class TfElement extends LitElement {
following: {type: Array},
users: {type: Object},
ids: {type: Array},
tags: {type: Array},
};
}
@ -32,8 +33,9 @@ class TfElement extends LitElement {
this.following = [];
this.users = {};
this.loaded = false;
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
this.tags = [];
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) {
self.set_hash(hash);
@ -64,6 +66,10 @@ class TfElement extends LitElement {
this.tab = 'search';
} else if (this.hash === '#connections') {
this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else {
this.tab = 'news';
}
@ -241,22 +247,47 @@ class TfElement extends LitElement {
if (confirm("Are you sure you want to create a new identity?")) {
await tfrpc.rpc.createIdentity();
this.ids = (await tfrpc.rpc.getIdentities()) || [];
if (this.ids && !this.whoami) {
this.whoami = this.ids[0];
}
}
}
render_id_picker() {
return html`
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
<button @click=${this.create_identity}>Create Identity</button>
<button @click=${this.create_identity} id="create_identity">Create Identity</button>
`;
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(`
WITH
recent AS (SELECT id, content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
FROM recent
WHERE json_extract(content, '$.channel') IS NOT NULL),
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
FROM recent, json_each(recent.content, '$.mentions') AS mention
WHERE json_valid(mention.value) AND tag LIKE '#%'),
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
}
async load() {
let whoami = this.whoami;
let tags = this.load_recent_tags();
let [following, users] = await this.following_deep([whoami], 2, {});
users = await this.fetch_about(following.sort(), users);
this.following = following;
this.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`);
this.whoami = whoami;
this.loaded = whoami;
@ -267,16 +298,24 @@ class TfElement extends LitElement {
let users = this.users;
if (this.tab === 'news') {
return html`
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
`;
} else if (this.tab === 'connections') {
return html`
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
`;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
`;
} else if (this.tab === 'search') {
return html`
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
`;
} else if (this.tab === 'query') {
return html`
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
`;
}
}
@ -286,6 +325,10 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
}
}
@ -293,7 +336,6 @@ class TfElement extends LitElement {
let self = this;
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
console.log(`starting loading ${this.whoami} ${this.loaded}`);
this.loading = true;
this.load().finally(function() {
self.loading = false;
@ -304,7 +346,9 @@ class TfElement extends LitElement {
<div>
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
<input type="button" class="tab" value="Query" ?disabled=${self.tab == 'query'} @click=${() => self.set_tab('query')}></input>
</div>
`;
let contents =
@ -316,9 +360,10 @@ class TfElement extends LitElement {
return html`
${this.render_id_picker()}
${tabs}
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
${contents}
`;
}
}
customElements.define('tf-app', TfElement);
customElements.define('tf-app', TfElement);

View File

@ -13,7 +13,7 @@ class TfComposeElement extends LitElement {
branch: {type: String},
apps: {type: Object},
drafts: {type: Object},
}
};
}
static styles = styles;
@ -56,7 +56,7 @@ class TfComposeElement extends LitElement {
if (!draft.mentions[link]) {
draft.mentions[link] = {
link: link,
}
};
}
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
updated = true;
@ -111,7 +111,7 @@ class TfComposeElement extends LitElement {
let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
resolve(result);
}
};
img.onerror = function(event) {
reject(new Error('Failed to load image.'));
};
@ -176,7 +176,7 @@ class TfComposeElement extends LitElement {
}
}
submit() {
async submit() {
let self = this;
let draft = this.get_draft();
let edit = this.renderRoot.getElementById('edit');
@ -195,14 +195,25 @@ class TfComposeElement extends LitElement {
message.contentWarning = draft.content_warning;
}
console.log('Would post:', message);
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
edit.value = '';
self.change();
self.notify(undefined);
self.requestUpdate();
}).catch(function(error) {
if (draft.encrypt_to) {
let to = new Set(draft.encrypt_to);
to.add(this.whoami);
to = [...to];
message.recps = to;
console.log('message is now', message);
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
console.log('encrypted as', message);
}
try {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
edit.value = '';
self.change();
self.notify(undefined);
self.requestUpdate();
});
} catch (error) {
alert(error.message);
});
}
}
discard() {
@ -226,12 +237,47 @@ class TfComposeElement extends LitElement {
input.click();
}
async autocomplete(text, callback) {
this.last_autocomplete = text;
let results = [];
try {
let rows = await tfrpc.rpc.query(`
SELECT messages.content FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ?
ORDER BY timestamp DESC LIMIT 10
`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]);
for (let row of rows) {
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
results.push({key: match[1], value: match[2]});
}
}
}
} finally {
if (this.last_autocomplete === text) {
callback(results);
}
}
}
firstUpdated() {
let tribute = new Tribute({
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
return `[@${item.original.key}](${item.original.value})`;
},
collection: [
{
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
return `[@${item.original.key}](${item.original.value})`;
},
},
{
trigger: '&',
values: this.autocomplete,
selectTemplate: function(item) {
return `![${item.original.key}](${item.original.value})`;
},
},
],
});
tribute.attach(this.renderRoot.getElementById('edit'));
}
@ -244,6 +290,16 @@ class TfComposeElement extends LitElement {
preview.innerHTML = this.process_text(edit.value);
this.last_updated_text = edit.value;
}
let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) {
let tribute = new Tribute({
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
return item.original.value;
},
});
tribute.attach(encrypt);
}
}
remove_mention(id) {
@ -256,9 +312,18 @@ class TfComposeElement extends LitElement {
render_mention(mention) {
let self = this;
return html`
<div>
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter(x => x[0] != 'name')
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
</div>
</div>
</div>`;
}
@ -283,11 +348,11 @@ class TfComposeElement extends LitElement {
};
}
}
let draft = this.get_draft();
let draft = self.get_draft();
draft.mentions = Object.assign(draft.mentions || {}, mentions);
this.requestUpdate();
this.notify(draft);
this.apps = null;
self.requestUpdate();
self.notify(draft);
self.apps = null;
}
if (this.apps) {
@ -304,13 +369,14 @@ class TfComposeElement extends LitElement {
}
render_attach_app_button() {
let self = this;
async function attach_app() {
this.apps = await tfrpc.rpc.apps();
self.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
} else {
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
}
}
@ -344,6 +410,45 @@ class TfComposeElement extends LitElement {
return this.drafts[this.branch || ''] || {};
}
update_encrypt(event) {
let input = event.srcElement;
let matches = input.value.match(/@.*?\.ed25519/g);
if (matches) {
let draft = this.get_draft();
let to = [...new Set(matches.concat(draft.encrypt_to))];
this.set_encrypt(to);
input.value = '';
}
}
render_encrypt() {
let draft = this.get_draft();
if (draft.encrypt_to === undefined) {
return;
}
return html`
<div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label>
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
<input type="button" value="🚮" @click=${() => this.set_encrypt(undefined)}></input>
</div>
<ul>
${draft.encrypt_to.map(x => html`
<li>
<tf-user id=${x} .users=${this.users}></tf-user>
<input type="button" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
</li>`)}
</ul>
`;
}
set_encrypt(encrypt) {
let draft = this.get_draft();
draft.encrypt_to = encrypt;
this.notify(draft);
this.requestUpdate();
}
render() {
let self = this;
let draft = self.get_draft();
@ -351,7 +456,11 @@ class TfComposeElement extends LitElement {
draft.content_warning !== undefined ?
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
undefined;
let encrypt = draft.encrypt_to !== undefined ?
undefined :
html`<input type="button" value="🔐" @click=${() => this.set_encrypt([])}></input>`;
let result = html`
${this.render_encrypt()}
<div style="display: flex; flex-direction: row; width: 100%">
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
<div style="flex: 1 0 50%">
@ -362,13 +471,14 @@ class TfComposeElement extends LitElement {
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_content_warning()}
${this.render_attach_app()}
<input type="button" value="Submit" @click=${this.submit}></input>
<input type="button" id="submit" value="Submit" @click=${this.submit}></input>
<input type="button" value="Attach" @click=${this.attach}></input>
${this.render_attach_app_button()}
${encrypt}
<input type="button" value="Discard" @click=${this.discard}></input>
`;
return result;
}
}
customElements.define('tf-compose', TfComposeElement);
customElements.define('tf-compose', TfComposeElement);

View File

@ -9,12 +9,11 @@ class TfIdentityPickerElement extends LitElement {
return {
ids: {type: Array},
selected: {type: String},
}
};
}
constructor() {
super();
let self = this;
this.ids = [];
}
@ -34,4 +33,4 @@ class TfIdentityPickerElement extends LitElement {
}
}
customElements.define('tf-id-picker', TfIdentityPickerElement);
customElements.define('tf-id-picker', TfIdentityPickerElement);

View File

@ -11,10 +11,10 @@ class TfMessageElement extends LitElement {
message: {type: Object},
users: {type: Object},
drafts: {type: Object},
raw: {type: Boolean},
format: {type: String},
blog_data: {type: String},
expanded: {type: Object},
}
};
}
static styles = styles;
@ -26,12 +26,14 @@ class TfMessageElement extends LitElement {
this.message = {};
this.users = {};
this.drafts = {};
this.raw = false;
this.format = 'message';
this.expanded = {};
}
show_reply() {
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
encrypt_to: this.message?.decrypted?.recps,
}}});
this.dispatchEvent(event);
}
@ -69,12 +71,12 @@ class TfMessageElement extends LitElement {
hash: this.message?.hash,
content: this.message?.content,
signature: this.message?.signature,
}
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
};
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
}
vote(emoji) {
let reaction = emoji.emoji;
let reaction = emoji;
let message = this.message.id;
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
tfrpc.rpc.appendMessage(
@ -127,6 +129,13 @@ class TfMessageElement extends LitElement {
body_click(event) {
if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src);
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
let next = event.srcElement.nextSibling;
if (next.style.display == 'block') {
next.style.display = 'none';
} else {
next.style.display = 'block';
}
}
}
@ -168,10 +177,7 @@ class TfMessageElement extends LitElement {
render_mentions() {
let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(x =>
x.name?.startsWith('audio:') ||
x.name?.startsWith('video:') ||
this.message?.content?.text?.indexOf(x.link) === -1);
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
if (mentions.length) {
let self = this;
return html`
@ -214,22 +220,59 @@ class TfMessageElement extends LitElement {
}
}
render_channels() {
let content = this.message?.content;
if (this?.messsage?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let channels = [];
if (typeof content.channel === 'string') {
channels.push(`#${content.channel}`);
}
if (Array.isArray(content.mentions)) {
for (let mention of content.mentions) {
if (typeof mention?.link === 'string' &&
mention.link.startsWith('#')) {
channels.push(mention.link);
}
}
}
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let self = this;
let raw_button = this.raw ?
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
let raw_button;
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<input type="button" value="Markdown" @click=${() => self.format = 'md'}></input>`;
} else {
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
}
break;
case 'md':
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
break;
default:
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
break;
}
function small_frame(inner) {
let body;
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); 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"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
${self.raw ? self.render_raw() : inner}
${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()}
</div>
`
`;
}
if (this.message?.type === 'contact_group') {
return html`
@ -265,7 +308,7 @@ class TfMessageElement extends LitElement {
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
</div>
`
`;
}
let update = content.about == this.message.author ?
html`<div style="font-weight: bold">Updated profile.</div>` :
@ -304,14 +347,24 @@ class TfMessageElement extends LitElement {
<input type="button" value="Reply" @click=${this.show_reply}></input>
`;
let self = this;
let body = this.raw ?
this.render_raw() :
unsafeHTML(tfutils.markdown(content.text));
let body;
switch (this.format) {
case 'raw':
body = this.render_raw();
break;
case 'md':
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
break;
case 'message':
body = unsafeHTML(tfutils.markdown(content.text));
break;
}
let content_warning = html`
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
`;
let content_html =
html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
@ -324,6 +377,8 @@ class TfMessageElement extends LitElement {
` :
content_warning :
content_html;
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
@ -339,9 +394,10 @@ class TfMessageElement extends LitElement {
display: block;
}
</style>
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span>${raw_button}</span>
@ -355,6 +411,40 @@ class TfMessageElement extends LitElement {
${this.render_children()}
</div>
`;
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span>${raw_button}</span>
</div>
${content.text}
${this.render_votes()}
<div>
<input type="button" value="React" @click=${this.react}></input>
</div>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function(data) {
@ -364,9 +454,16 @@ class TfMessageElement extends LitElement {
this.expanded[(this.message.id || '') + ':blog'] ?
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
undefined;
let body = this.raw ?
this.render_raw() :
html`
let body;
switch (this.format) {
case 'raw':
body = this.render_raw();
break;
case 'md':
body = content.summary;
break;
case 'message':
body = html`
<div
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
@click=${x => self.toggle_expanded(':blog')}>
@ -378,6 +475,8 @@ class TfMessageElement extends LitElement {
</div>
${payload}
`;
break;
}
return html`
<style>
code {
@ -426,7 +525,11 @@ class TfMessageElement extends LitElement {
</div>
`);
} else if (typeof(this.message.content) == 'string') {
return small_frame(html`<span>🔒</span>`);
if (this.message?.decrypted) {
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
} else {
return small_frame(html`<span>🔒</span>`);
}
} else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
}

View File

@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
}
};
}
static styles = styles;

View File

@ -11,7 +11,7 @@ class TfProfileElement extends LitElement {
id: {type: String},
users: {type: Object},
size: {type: Number},
}
};
}
static styles = styles;
@ -33,7 +33,7 @@ class TfProfileElement extends LitElement {
contact: this.id,
}, change)).catch(function(error) {
alert(error?.message);
})
});
}
follow() {
@ -148,6 +148,10 @@ class TfProfileElement extends LitElement {
<div><label for="description">Description:</label></div>
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
</div>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
</div>
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
</div>` : null;
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;

View File

@ -36,4 +36,19 @@ img {
padding: 8px;
margin: 4px;
}
div.img_caption {
color: #888;
cursor: pointer;
}
div.img_caption::after {
content: ' ±';
}
blockquote {
border-left: 4px solid #fff;
margin-left: 0px;
padding-left: 8px;
}
`;

View File

@ -9,7 +9,7 @@ class TfTabConnectionsElement extends LitElement {
connections: {type: Array},
stored_connections: {type: Array},
users: {type: Object},
}
};
}
constructor() {
@ -42,11 +42,8 @@ class TfTabConnectionsElement extends LitElement {
let self = this;
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
if (peers.length) {
return html`
<ul>
${peers.map(x => html`${self.render_room_peer(x)}`)}
</ul>
`;
let connections = this.connections.map(x => x.id);
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
}
}
@ -59,7 +56,7 @@ class TfTabConnectionsElement extends LitElement {
return html`
<li>
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
</li>
`;
}
@ -71,7 +68,7 @@ class TfTabConnectionsElement extends LitElement {
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</li>
`
`;
}
async forget_stored_connection(connection) {
@ -79,6 +76,18 @@ class TfTabConnectionsElement extends LitElement {
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
}
render_connection(connection) {
return html`
<input type="button" @click=${() => tfrpc.rpc.closeConnection(connection.id)} value="Close"></input>
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
<ul>
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)}
</ul>
`;
}
render() {
let self = this;
return html`
@ -93,12 +102,8 @@ class TfTabConnectionsElement extends LitElement {
</ul>
<h2>Connections</h2>
<ul>
${this.connections.map(x => html`
<li>
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
<tf-user id=${x} .users=${this.users}></tf-user>
${self.render_room_peers(x)}
</li>
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
<li>${this.render_connection(x)}</li>
`)}
</ul>
<h2>Stored Connections (WIP)</h2>

View File

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

View File

@ -0,0 +1,190 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabNewsFeedElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
hash: {type: String},
following: {type: Array},
messages: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.hash = '#';
this.following = [];
this.drafts = {};
this.expanded = {};
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
}
async fetch_messages() {
if (this.hash.startsWith('#@')) {
let r = await tfrpc.rpc.query(
`
WITH mine AS (SELECT messages.*
FROM messages
WHERE messages.author = ?
ORDER BY sequence DESC
LIMIT 20)
SELECT messages.*
FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT * FROM mine
`,
[
this.hash.substring(1),
]);
return r;
} else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query(
`
SELECT messages.*
FROM messages
WHERE id = ?1
UNION
SELECT messages.*
FROM messages JOIN messages_refs
ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1
`,
[
this.hash.substring(1),
]);
} else {
return await tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following),
this.start_time,
/*
** Don't show messages more than a day into the future to prevent
** messages with far-future timestamps from staying at the top forever.
*/
new Date().valueOf() + 24 * 60 * 60 * 1000,
]);
}
}
async load_more() {
let last_start_time = this.start_time;
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
let more = await tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ?
AND messages.timestamp <= ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following),
this.start_time,
last_start_time,
]);
this.messages = await this.decrypt([...more, ...this.messages]);
}
async decrypt(messages) {
let result = [];
for (let message of messages) {
let content;
try {
content = JSON.parse(message?.content);
} catch {
}
if (typeof(content) === 'string') {
let decrypted;
try {
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
} catch {
}
if (decrypted) {
try {
message.decrypted = JSON.parse(decrypted);
} catch {
message.decrypted = decrypted;
}
}
}
result.push(message);
}
return result;
}
async add_messages(messages) {
this.messages = await this.decrypt([...messages, ...this.messages]);
}
render() {
if (!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following) {
console.log(`loading messages for ${this.whoami}`);
let self = this;
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
}).catch(function(error) {
alert(JSON.stringify(error, null, 2));
});
}
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html`
<input type="button" value="Load More" @click=${this.load_more}></input>
`;
}
return html`
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
${more}
`;
}
}
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);

View File

@ -2,114 +2,6 @@ import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabNewsFeedElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
hash: {type: String},
following: {type: Array},
messages: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
}
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.hash = '#';
this.following = [];
this.drafts = {};
this.expanded = {};
}
async fetch_messages() {
if (this.hash.startsWith('#@')) {
let r = await tfrpc.rpc.query(
`
WITH mine AS (SELECT messages.*
FROM messages
WHERE messages.author = ?
ORDER BY sequence DESC
LIMIT 20)
SELECT messages.*
FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT * FROM mine
`,
[
this.hash.substring(1),
]);
return r;
} else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query(
`
SELECT messages.*
FROM messages
WHERE id = ?1
UNION
SELECT messages.*
FROM messages JOIN messages_refs
ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1
`,
[
this.hash.substring(1),
]);
} else {
return await tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following),
new Date().valueOf() - 24 * 60 * 60 * 1000,
]);
}
}
render() {
if (!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following) {
console.log(`loading messages for ${this.whoami}`);
let self = this;
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages().then(function(messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
}).catch(function(error) {
alert(JSON.stringify(error, null, 2));
});
}
return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>`;
}
}
class TfTabNewsElement extends LitElement {
static get properties() {
return {
@ -120,7 +12,7 @@ class TfTabNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
}
};
}
static styles = styles;
@ -141,12 +33,22 @@ class TfTabNewsElement extends LitElement {
});
}
connectedCallback() {
super.connectedCallback();
document.body.addEventListener('keypress', this.on_keypress.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
}
show_more() {
let unread = this.unread;
let news = this.renderRoot?.getElementById('news');
let news = this.shadowRoot?.getElementById('news');
if (news) {
console.log('injecting messages', news.messages);
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
this.dispatchEvent(new CustomEvent('refresh'));
}
}
@ -193,6 +95,13 @@ class TfTabNewsElement extends LitElement {
}
}
on_keypress(event) {
if (event.target === document.body &&
event.key == '.') {
this.show_more();
}
}
render() {
let profile = this.hash.startsWith('#@') ?
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
@ -200,12 +109,11 @@ class TfTabNewsElement extends LitElement {
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
${profile}
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
`;
}
}
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
customElements.define('tf-tab-news', TfTabNewsElement);
customElements.define('tf-tab-news', TfTabNewsElement);

114
apps/ssb/tf-tab-query.js Normal file
View File

@ -0,0 +1,114 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabQueryElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
following: {type: Array},
query: {type: String},
expanded: {type: Object},
results: {type: Array},
error: {type: Object},
duration: {type: Number},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
this.duration = undefined;
}
async search(query) {
console.log('Searching...', this.whoami, query);
this.results = [];
this.error = undefined;
this.duration = undefined;
let search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
let start_time = new Date();
try {
this.results = await tfrpc.rpc.query(query, [])
} catch (error) {
this.error = error;
}
let end_time = new Date();
this.duration = (end_time - start_time).valueOf();
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
}
search_keydown(event) {
if (event.keyCode == 13 && event.ctrlKey) {
this.query = this.renderRoot.getElementById('search').value;
event.preventDefault();
}
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render_results() {
if (!this.results?.length) {
return html`<div>No results.</div>`;
} else {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
</table>`;
}
}
render_error() {
if (this.error) {
return html`<h2 style="color: red">${this.error.message}</h2>
<pre style="color: red">${this.error.stack}</pre>`;
}
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
}
let self = this;
return html`
<div style="display: flex; flex-direction: row">
<textarea id="search" rows=8 style="flex: 1" @keydown=${this.search_keydown}>${this.query}</textarea>
<input type="button" value="Execute" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
</div>
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div>
${this.render_error()}
${this.render_results()}
`;
}
}
customElements.define('tf-tab-query', TfTabQueryElement);

View File

@ -9,7 +9,8 @@ class TfTabSearchElement extends LitElement {
users: {type: Object},
following: {type: Array},
query: {type: String},
}
expanded: {type: Object},
};
}
static styles = styles;
@ -20,6 +21,7 @@ class TfTabSearchElement extends LitElement {
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
}
async search(query) {
@ -55,8 +57,20 @@ class TfTabSearchElement extends LitElement {
}
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
}
let self = this;
@ -65,7 +79,7 @@ class TfTabSearchElement extends LitElement {
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`;
}
}

24
apps/ssb/tf-tag.js Normal file
View File

@ -0,0 +1,24 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles} from './tf-styles.js';
class TfTagElement extends LitElement {
static get properties() {
return {
tag: {type: String},
count: {type: Number},
};
}
static styles = styles;
constructor() {
super();
}
render() {
let number = this.count ? html` (${this.count})` : undefined;
return html`<a href="#q=${this.tag}" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px">${this.tag}${number}</a>`;
}
}
customElements.define('tf-tag', TfTagElement);

View File

@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
return {
id: {type: String},
users: {type: Object},
}
};
}
static styles = styles;

View File

@ -1,9 +1,54 @@
import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js';
function image(node, entering) {
if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')) {
if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')) {
if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
writer.image = image;
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
parsed = hashtagify.transform(parsed);

View File

@ -114,13 +114,12 @@ class TodoListElement extends LitElement {
@change=${event => self.input_change(event, item)}
@keydown=${event => self.input_keydown(event, item)}
@blur=${x => self.input_blur(item)}></input>
<span @click=${x => self.remove_item(item)}>x</span></div>
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
`;
} else {
return html`
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
<span @click=${x => self.editing = index}>${item.text}</span>
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
<span @click=${x => self.editing = index}>${item.text || '(empty)'}</span>
`;
}
}
@ -175,7 +174,8 @@ class TodoListElement extends LitElement {
return html`
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
${name}
${(this.items || []).map(x => self.render_item(x))}
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
<button @click=${self.add_item}>+ Item</button>
<button @click=${self.remove_list}>- List</button>
</div>

View File

@ -61,8 +61,7 @@ function socket(request, response, client) {
let process;
let options = {};
let credentials = auth.query(request.headers);
let refresh_token = credentials?.refresh?.token;
let refresh_interval = credentials?.refresh?.interval;
let refresh = auth.make_refresh(credentials);
response.onClose = async function() {
if (process && process.task) {
@ -125,9 +124,14 @@ function socket(request, response, client) {
options.credentials = credentials;
options.packageOwner = packageOwner;
options.packageName = packageName;
options.url = message.url;
let sessionId = makeSessionId();
if (blobId) {
process = await core.getSessionProcessBlob(blobId, sessionId, options);
if (message.edit_only) {
response.send(JSON.stringify({action: 'ready', edit_only: true}), 0x1);
} else {
process = await core.getSessionProcessBlob(blobId, sessionId, options);
}
}
if (process) {
process.app.readOutput(function(message) {
@ -198,9 +202,9 @@ function socket(request, response, client) {
}
}
if (refresh_token) {
if (refresh) {
return {
'Set-Cookie': `session=${refresh_token}; path=/; Max-Age=${refresh_interval}; Secure; SameSite=Strict`,
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
};
}
}

View File

@ -2,18 +2,137 @@
<html>
<head>
<title>Tilde Friends Sign-in</title>
<script>
function showHideConfirm() {
document.getElementById("confirmPassword").style.display = document.getElementById("register").checked ? "block" : "none";
}
</script>
<link type="text/css" rel="stylesheet" href="/static/style.css">
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--HEAD-->
</head>
<body>
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
<div id="content"><!--SESSION--></div>
<tf-auth id="auth"></tf-auth>
<script>window.litDisableBundleWarning = true;</script>
<script type="module">
import {LitElement, html} from '/static/lit/lit-all.min.js';
let g_data = $AUTH_DATA;
let app = document.getElementById('auth');
Object.assign(app, g_data);
class TfAuthElement extends LitElement {
static get_properties() {
return {
name: {type: String},
tab: {type: String},
};
}
constructor() {
super();
this.tab = 'login';
}
tab_changed(name) {
this.tab = name;
this.requestUpdate();
}
render() {
let self = this;
return html`
<style>
[name="tab"] {
display: none;
}
[name="tab"]+label {
background-color: #586e75;
padding: 1em;
display: inline-block;
flex: 1 0;
text-align: center;
cursor: pointer;
}
[name="tab"]+label:hover {
background-color: #dc322f;
}
[name="tab"]:checked+label {
background-color: #93a1a1;
border: 2px solid #eee8d5;
}
.error {
color: #f00;
border: 1px solid #f00;
margin: 4px;
padding: 8px;
}
form label {
padding-top: 1em;
font-weight: bold;
}
form input {
font-size: x-large;
padding: 4px;
}
input[type="submit"] {
margin-top: 1em;
margin-bottom: 1em;
padding: 1em;
}
</style>
<div style="display: flex; flex-direction: column; max-width: 1280px; margin: auto">
<h1 ?hidden=${this.name}>Welcome.</h1>
<h1 ?hidden=${this.name === undefined}>Welcome, ${this.name}.</h1>
<div style="display: flex; flex-direction: row; width: 100%">
<input type="radio" name="tab" id="login" value="Login" ?checked=${this.tab == 'login'} @change=${() => self.tab_changed('login')}></input>
<label for="login" id="login_label">Login</label>
<input type="radio" name="tab" id="register" value="Register" ?checked=${this.tab == 'register'} @change=${() => self.tab_changed('register')}></input>
<label for="register" id="register_label">Register</label>
<input type="radio" name="tab" id="guest" value="Guest" ?checked=${this.tab == 'guest'} @change=${() => self.tab_changed('guest')}></input>
<label for="guest" id="guest_label">Guest</label>
<input type="radio" name="tab" id="change" value="Change Password" ?checked=${this.tab == 'change'} @change=${() => self.tab_changed('change')}></input>
<label for="change" id="change_label">Change Password</label>
</div>
<div ?hidden=${this.tab != 'login' && this.tab != 'register' && this.tab != 'change'}>
<div id="error" ?hidden=${this.error === undefined} class="error">
${this.error}
</div>
<form method="POST" style="display: flex; flex-direction: column">
<label for="name">Name:</label>
<input type="text" id="name" name="name"></input>
<label for="password">${this.tab == 'change' ? 'Old ' : ''}Password:</label>
<input type="password" id="password" name="password"></input>
<label ?hidden=${this.tab != 'change'} for="new_password">New Password:</label>
<input ?hidden=${this.tab != 'change'} type="password" id="new_password" name="new_password"></input>
<label ?hidden=${this.tab != 'register' && this.tab != 'change'} for="confirm">Confirm ${this.tab == 'change' ? 'New ' : ''}Password:</label>
<input ?hidden=${this.tab != 'register' && this.tab != 'change'} type="password" id="confirm" name="confirm"></input>
<input id="loginButton" type="submit" name="submit" value="Login"></input>
<input type="hidden" name="register" value="${this.tab == 'register' ? 1 : 0}"></input>
<input type="hidden" name="change" value="${this.tab == 'change' ? 1 : 0}"></input>
</form>
</div>
<div ?hidden=${this.tab != 'guest'}>
<form method="POST" style="display: flex; flex-direction: column">
<input type="submit" id="guestButton" name="guestButton" value="Proceed as Guest"></input>
</form>
</div>
<div ?hidden=${this.have_administrator} class="notice">
There is currently no administrator. You will be made administrator.
</div>
<h2>Code of Conduct</h2>
<textarea readonly rows="20" cols="80" style="resize: none">${this.code_of_conduct}</textarea>
</div>
`;
}
}
customElements.define('tf-auth', TfAuthElement);
</script>
</body>
</html>

View File

@ -113,22 +113,37 @@ function getCookies(headers) {
return cookies;
}
function isNameValid(name) {
let c = name.charAt(0);
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9'));
}
function handler(request, response) {
let session = getCookies(request.headers).session;
if (request.uri == "/login") {
let formData = form.decodeForm(request.query);
if (query(request.headers)?.permissions?.authenticated) {
if (formData.return) {
response.writeHead(303, {"Location": formData.return});
} else {
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"});
}
response.end();
return;
}
let sessionIsNew = false;
let loginError;
let formData = form.decodeForm(request.query);
if (request.method == "POST" || formData.submit) {
sessionIsNew = true;
formData = form.decodeForm(utf8Decode(request.body), formData);
if (formData.submit == "Login") {
let account = gDatabase.get("user:" + formData.name);
account = account ? JSON.parse(account) : account;
if (formData.register == "1") {
if (formData.register == '1') {
if (!account &&
isNameValid(formData.name) &&
formData.password == formData.confirm) {
let users = new Set();
let users_original = gDatabase.get('users');
@ -145,12 +160,23 @@ function handler(request, response) {
}
session = makeJwt({name: formData.name});
account = {password: hashPassword(formData.password)};
gDatabase.set("user:" + formData.name, JSON.stringify(account));
gDatabase.set('user:' + formData.name, JSON.stringify(account));
if (noAdministrator()) {
makeAdministrator(formData.name);
}
} else {
loginError = "Error registering account.";
loginError = 'Error registering account.';
}
} else if (formData.change == '1') {
if (account &&
isNameValid(formData.name) &&
formData.new_password == formData.confirm &&
verifyPassword(formData.password, account.password)) {
session = makeJwt({name: formData.name});
account = {password: hashPassword(formData.new_password)};
gDatabase.set('user:' + formData.name, JSON.stringify(account));
} else {
loginError = 'Error changing password.';
}
} else {
if (account &&
@ -161,7 +187,7 @@ function handler(request, response) {
makeAdministrator(formData.name);
}
} else {
loginError = "Invalid username or password.";
loginError = 'Invalid username or password.';
}
}
} else {
@ -178,46 +204,16 @@ function handler(request, response) {
} else {
File.readFile("core/auth.html").then(function(data) {
let html = utf8Decode(data);
let contents = "";
if (entry) {
if (sessionIsNew) {
contents += '<div>Welcome back, ' + entry.name + '.</div>\n';
} else {
contents += '<div>You are already logged in, ' + entry.name + '.</div>\n';
}
contents += '<div><a href="/login/logout">Logout</a></div>\n';
} else {
contents += '<form method="POST">\n';
if (loginError) {
contents += "<p>" + loginError + "</p>\n";
}
contents += '<div id="auth_greeting"><b>Halt. Who goes there?</b></div>\n'
contents += '<div id="auth">\n';
contents += '<div id="auth_login">\n'
if (noAdministrator()) {
contents += '<div class="notice">There is currently no administrator. You will be made administrator.</div>\n';
}
contents += '<div><label for="name">Name:</label> <input type="text" id="name" name="name" value=""></div>\n';
contents += '<div><label for="password">Password:</label> <input type="password" id="password" name="password" value=""></div>\n';
contents += '<div id="confirmPassword" style="display: none"><label for="confirm">Confirm:</label> <input type="password" id="confirm" name="confirm" value=""></div>\n';
contents += '<div><input type="checkbox" id="register" name="register" value="1" onchange="showHideConfirm()"> <label for="register">Register a new account</label></div>\n';
contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></div>\n';
contents += '</div>';
contents += '<div class="auth_or"> - or - </div>';
contents += '<div id="auth_guest">\n';
contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest">\n';
contents += '</div>\n';
contents += '</div>\n';
contents += '<div style="text-align: center">\n';
contents += '<h2>Code of Conduct</h2>\n';
contents += `<div><textarea readonly rows=20 cols=80>${core.globalSettings.code_of_conduct}</textarea></div>\n`;
contents += '</div>\n';
contents += '</form>';
}
let text = html.replace("<!--SESSION-->", contents);
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": text.length});
response.end(text);
let auth_data = {
session_is_new: sessionIsNew,
name: entry?.name,
error: loginError,
code_of_conduct: core.globalSettings.code_of_conduct,
have_administrator: !noAdministrator(),
};
html = utf8Encode(html.replace('$AUTH_DATA', JSON.stringify(auth_data)));
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": html.length});
response.end(html);
}).catch(function(error) {
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
response.end("404 File not found");
@ -260,12 +256,17 @@ function query(headers) {
return {
session: entry,
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
refresh: {
token: makeJwt({name: entry.name}),
interval: kRefreshInterval,
},
};
}
}
export { handler, query };
function make_refresh(credentials) {
if (credentials?.session?.name) {
return {
token: makeJwt({name: credentials.session.name}),
interval: kRefreshInterval,
};
}
}
export { handler, query, make_refresh };

View File

@ -1,14 +1,11 @@
import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';
let gSocket;
let gCredentials;
let gPermissions;
let gCurrentFile;
let gFiles = {};
let gApp = {files: {}, emoji: '📦'};
let gEditor;
let gSplit;
let gGraphs = {};
let gTimeSeries = {};
let gOriginalInput;
let kErrorColor = "#dc322f";
@ -26,6 +23,310 @@ const k_api = {
setHash: {args: ['hash'], func: api_setHash},
};
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
class TfNavigationElement extends LitElement {
static get properties() {
return {
credentials: {type: Object},
permissions: {type: Object},
show_permissions: {type: Boolean},
status: {type: Object},
spark_lines: {type: Object},
version: {type: Object},
show_version: {type: Boolean},
};
}
constructor() {
super();
this.permissions = {};
this.show_permissions = false;
this.status = {};
this.spark_lines = {};
}
toggle_edit(event) {
event.preventDefault();
if (editing()) {
closeEditor();
} else {
edit();
}
}
reset_permission(key) {
send({action: "resetPermission", permission: key});
}
get_spark_line(key, options) {
if (!this.spark_lines[key]) {
let spark_line = document.createElement('tf-sparkline');
spark_line.style.display = 'flex';
spark_line.style.flexDirection = 'row';
spark_line.style.flex = '0 50 5em';
spark_line.title = key;
if (options) {
if (options.max) {
spark_line.max = options.max;
}
}
this.spark_lines[key] = spark_line;
this.requestUpdate();
}
return this.spark_lines[key];
}
render_login() {
if (this?.credentials?.session?.name) {
return html`<a id="login" href="/login/logout?return=${url() + hash()}">logout ${this.credentials.session.name}</a>`;
} else {
return html`<a id="login" href="/login?return=${url() + hash()}">login</a>`;
}
}
render_permissions() {
if (this.show_permissions) {
return html`
<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
<div>This app has the following permissions:</div>
${Object.keys(this.permissions).map(key => html`
<div>
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
<button @click=${() => this.reset_permission(key)}>Reset</button>
</div>
`)}
<button @click=${() => this.show_permissions = false}>Close</button>
</div>
</div>
`;
}
}
render() {
let self = this;
return html`
<style>
${k_global_style}
</style>
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px; align-items: center">
<span style="cursor: pointer" @click=${() => this.show_version = !this.show_version}>😎</span>
<span ?hidden=${!this.show_version} style="flex: 0 0; white-space: nowrap" title=${this.version?.name + ' ' + Object.entries(this.version || {}).filter(x => ['name', 'number'].indexOf(x[0]) == -1).map(x => `\n* ${x[0]}: ${x[1]}`)}>${this.version?.number}</span>
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
<span id="requests"></span>
${this.render_permissions()}
<span style="flex: 1 1; display: flex; flex-direction: row; white-space: nowrap; margin: 0; padding: 0">${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [html`<span style="font-size: xx-small">${x.dataset.emoji}</span>`, x])}</span>
<span style="flex: 0 0; white-space: nowrap">${this.render_login()}</span>
</div>
`;
}
}
customElements.define('tf-navigation', TfNavigationElement);
class TfFilesElement extends LitElement {
static get properties() {
return {
current: {type: String},
files: {type: Object},
};
}
constructor() {
super();
this.files = {};
}
file_click(file) {
this.dispatchEvent(new CustomEvent('file_click', {
detail: {
file: file,
},
bubbles: true,
composed: true,
}));
}
render_file(file) {
let classes = ['file'];
if (file == this.current) {
classes.push('current');
}
if (!this.files[file].clean) {
classes.push('dirty');
}
return html`<div class="${classes.join(' ')}" @click=${x => this.file_click(file)}>${file}</div>`;
}
render() {
let self = this;
return html`
<style>
div.file {
padding: 0.5em;
cursor: pointer;
}
div.file:hover {
background-color: #1a9188;
}
div.file::before {
content: '📄 ';
}
div.file.current {
font-weight: bold;
background-color: #2aa198;
}
div.file.dirty::after {
content: '*';
}
</style>
<div>
${Object.keys(this.files).sort().map(x => self.render_file(x))}
</div>
`;
}
}
customElements.define('tf-files', TfFilesElement);
class TfFilesPaneElement extends LitElement {
static get properties() {
return {
expanded: {type: Boolean},
current: {type: String},
files: {type: Object},
};
}
constructor() {
super();
this.expanded = window.localStorage.getItem('files') != '0';
this.files = {};
}
set_expanded(expanded) {
this.expanded = expanded;
window.localStorage.setItem('files', expanded ? '1' : '0');
}
render() {
let self = this;
let expander = this.expanded ?
html`<span @click=${() => self.set_expanded(false)} class="expander">«</span>` :
html`<span @click=${() => self.set_expanded(true)} class="expander">»</span>`;
let content = html`
<div id="files_content">
<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
<br>
<div><button @click=${() => newFile()}>New File</button></div>
<div><button @click=${() => removeFile()}>Remove File</button></div>
</div>
`;
return html`
<style>
.expander {
font-weight: bold;
width: 100%;
right: 0;
flex: 0;
padding: 0.25em;
cursor: pointer;
}
</style>
<div>
<div style="display: flex; flex-direction: row">
${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
${expander}
</div>
${this.expanded ? content : undefined}
</div>
`;
}
}
customElements.define('tf-files-pane', TfFilesPaneElement);
class TfSparkLineElement extends LitElement {
static get properties() {
return {
lines: {type: Array},
min: {type: Number},
max: {type: Number},
};
}
constructor() {
super();
this.min = 0;
this.max = 1.0;
this.lines = [];
this.k_values_max = 100;
}
append(key, value) {
let line = null;
for (let it of this.lines) {
if (it.name == key) {
line = it;
break;
}
}
if (!line) {
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
line = {
name: key,
style: k_colors[this.lines.length % k_colors.length],
values: Array(this.k_values_max).fill(0),
};
this.lines.push(line);
}
if (line.values.length >= this.k_values_max) {
line.values.shift();
}
line.values.push(value);
this.requestUpdate();
}
render_line(line) {
if (line?.values?.length >= 2) {
let max = Math.max(this.max, ...line.values);
let points = [].concat(...line.values.map((x, i) => [50.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (max - this.min)]));
return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`;
}
}
render() {
let max = Math.round(10.0 * Math.max(...this.lines.map(line => line.values[line.values.length - 1]))) / 10.0;
return html`
<svg style="max-width: 7.5em; max-height: 1.5em; margin: 0; padding: 0; background: #000" viewBox="0 0 50 10" xmlns="http://www.w3.org/2000/svg">
${this.lines.map(x => this.render_line(x))}
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">${max}</text>
</svg>
`;
}
}
customElements.define('tf-sparkline', TfSparkLineElement);
window.addEventListener("keydown", function(event) {
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
if (editing()) {
@ -81,12 +382,8 @@ function editing() {
return document.getElementById("editPane").style.display != 'none';
}
function toggleEdit() {
if (editing()) {
closeEditor();
} else {
edit();
}
function is_edit_only() {
return window.location.search == '?editonly=1' || window.innerWidth < 1024;
}
function edit() {
@ -95,11 +392,8 @@ function edit() {
}
window.localStorage.setItem('editing', '1');
if (gSplit) {
gSplit.destroy();
gSplit = undefined;
}
gSplit = Split(['#editPane', '#viewPane'], {minSize: 0});
document.getElementById("editPane").style.display = 'flex';
document.getElementById('viewPane').style.display = is_edit_only() ? 'none' : 'flex';
ensureLoaded([
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
@ -107,6 +401,7 @@ function edit() {
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}},
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
@ -118,6 +413,9 @@ function edit() {
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/lint.js"}},
{tagName: "script", attributes: {src: "/codemirror/jshint.js"}},
{tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}},
], function() {
load().catch(function(error) {
alert(error);
@ -126,40 +424,10 @@ function edit() {
});
}
function hideFiles() {
window.localStorage.setItem('files', '0');
document.getElementById('filesPane').classList.add('collapsed');
}
function showFiles() {
window.localStorage.setItem('files', '1');
document.getElementById('filesPane').classList.remove('collapsed');
}
function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
}
function stats() {
window.localStorage.setItem('stats', '1');
document.getElementById("statsPane").style.display = 'flex';
send({action: 'enableStats', enabled: true});
}
function closeStats() {
window.localStorage.setItem('stats', '0');
document.getElementById("statsPane").style.display = 'none';
send({action: 'enableStats', enabled: false});
}
function toggleStats() {
if (document.getElementById("statsPane").style.display == 'none') {
stats();
} else {
closeStats();
}
}
function guessMode(name) {
return name.endsWith(".js") ? "javascript" :
name.endsWith(".html") ? "htmlmixed" :
@ -175,7 +443,6 @@ function loadFile(name, id) {
}).then(function(text) {
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
if (!Object.values(gFiles).some(x => !x.doc)) {
document.getElementById("editPane").style.display = 'flex';
openFile(Object.keys(gFiles).sort()[0]);
}
});
@ -200,6 +467,13 @@ function load(path) {
'indentUnit': 4,
'indentWithTabs': true,
'showTrailingSpace': true,
'gutters': ['CodeMirror-lint-markers'],
'mode': {'js': 'javascript'}[(path || url()).split('.').pop()],
'lint': {
'options': {
'esversion': 2021,
},
},
});
gEditor.on('changes', function() {
updateFiles();
@ -238,10 +512,7 @@ function load(path) {
function closeEditor() {
window.localStorage.setItem('editing', '0');
document.getElementById("editPane").style.display = 'none';
if (gSplit) {
gSplit.destroy();
gSplit = undefined;
}
document.getElementById('viewPane').style.display = 'flex';
}
function explodePath() {
@ -401,7 +672,8 @@ function api_localStorageGet(key) {
}
function api_requestPermission(permission, id) {
let permissions = document.getElementById('permissions');
let outer = document.createElement('div');
outer.classList.add('permissions');
let container = document.createElement('div');
container.classList.add('permissions_contents');
@ -428,11 +700,13 @@ function api_requestPermission(permission, id) {
const k_options = [
{
id: 'allow',
text: '✅ Allow',
grant: ['allow once', 'allow'],
},
{
id: 'deny',
text: '❌ Deny',
grant: ['deny once', 'deny'],
},
@ -443,19 +717,17 @@ function api_requestPermission(permission, id) {
for (let option of k_options) {
let button = document.createElement('button');
button.innerText = option.text;
button.id = option.id;
button.onclick = function() {
resolve(option.grant[check.checked ? 1 : 0]);
while (permissions.firstChild) {
permissions.removeChild(permissions.firstChild);
}
permissions.style.visibility = 'hidden';
document.body.removeChild(outer);
}
div.appendChild(button);
}
container.appendChild(div);
outer.appendChild(container);
permissions.appendChild(container);
permissions.style.visibility = 'visible';
document.body.appendChild(outer);
});
}
@ -467,85 +739,20 @@ function api_setHash(hash) {
window.location.hash = hash;
}
function hidePermissions() {
let permissions = document.getElementById('permissions_settings');
while (permissions.firstChild) {
permissions.removeChild(permissions.firstChild);
}
permissions.style.visibility = 'hidden';
}
function showPermissions() {
let permissions = document.getElementById('permissions_settings');
let container = document.createElement('div');
container.classList.add('permissions_contents');
let div = document.createElement('div');
div.appendChild(document.createTextNode('This app has the following permission:'));
for (let key of Object.keys(gPermissions || {})) {
let row = document.createElement('div');
let span = document.createElement('span');
span.appendChild(document.createTextNode(key));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(': '));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(gPermissions[key] ? '✅ Allowed' : '❌ Denied'));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(' '));
row.appendChild(span);
let button = document.createElement('button');
button.innerText = 'Reset';
button.onclick = function() {
send({action: "resetPermission", permission: key});
};
row.appendChild(button);
div.appendChild(row);
}
container.appendChild(div);
div = document.createElement('div');
let button = document.createElement('button');
button.innerText = 'Close';
button.onclick = function() {
hidePermissions();
}
div.appendChild(button);
container.appendChild(div);
permissions.appendChild(container);
permissions.style.visibility = 'visible';
}
function _receive_websocket_message(message) {
if (message && message.action == "session") {
setStatusMessage("🟢 Executing...", kStatusColor);
gCredentials = message.credentials;
updateLogin();
document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials;
} else if (message && message.action == 'permissions') {
gPermissions = message.permissions;
let permissions = document.getElementById('permissions_settings');
if (permissions.firstChild) {
hidePermissions();
showPermissions();
}
document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {};
} else if (message && message.action == "ready") {
setStatusMessage(null);
if (window.location.hash) {
send({event: "hashChange", hash: window.location.hash});
}
if (window.localStorage.getItem('stats') == '1') {
/* Stats were opened before we connected. */
send({action: 'enableStats', enabled: true});
}
document.getElementsByTagName('tf-navigation')[0].version = message.version;
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") {
@ -566,8 +773,8 @@ function _receive_websocket_message(message) {
tls_malloc_percent: {group: 'memory', name: 'tls'},
uv_malloc_percent: {group: 'memory', name: 'uv'},
messages_stored: {group: 'stored', name: 'messages'},
blobs_stored: {group: 'stored', name: 'blobs'},
messages_stored: {group: 'store', name: 'messages'},
blobs_stored: {group: 'store', name: 'blobs'},
socket_count: {group: 'socket', name: 'total'},
socket_open_count: {group: 'socket', name: 'open'},
@ -577,56 +784,16 @@ function _receive_websocket_message(message) {
};
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
let graph_key = k_groups[key]?.group || key;
let graph = gGraphs[graph_key];
if (!graph) {
graph = {
chart: new SmoothieChart({
millisPerPixel: 100,
minValue: 0,
grid: {
millisPerLine: 1000,
verticalSections: 10,
},
tooltip: true,
}),
canvas: document.createElement('canvas'),
title: document.createElement('div'),
series: [],
};
gGraphs[graph_key] = graph;
graph.canvas.width = 240;
graph.canvas.height = 64;
graph.title.innerText = graph_key;
graph.title.style.flex = '0';
document.getElementById('graphs').appendChild(graph.title);
document.getElementById('graphs').appendChild(graph.canvas);
graph.chart.streamTo(graph.canvas, 1000);
if (['cpu', 'rpc', 'store', 'memory'].indexOf(graph_key) != -1) {
let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 });
line.dataset.emoji = {
'cpu': '💻',
'rpc': '🔁',
'store': '💾',
'memory': '🐏',
}[graph_key];
line.append(key, message.stats[key]);
}
let timeseries = gTimeSeries[key];
if (!timeseries) {
let is_multi = key != graph_key || graph.series.length > 1;
timeseries = new TimeSeries();
gTimeSeries[key] = timeseries;
graph.chart.addTimeSeries(timeseries, {lineWidth: 2, strokeStyle: is_multi ? k_colors[graph.series.length] : '#fff'});
graph.series.push(k_groups[key]?.name || key);
if (is_multi) {
while (graph.title.firstChild) {
graph.title.removeChild(graph.title.firstChild);
}
function makeColoredText(text, color) {
let element = document.createElement('span');
element.style.color = color;
element.innerText = text;
return element;
}
graph.title.appendChild(makeColoredText(graph_key + ':', '#fff'));
for (let series of graph.series) {
graph.title.appendChild(makeColoredText(' ' + series, k_colors[graph.series.indexOf(series)]));
}
}
}
timeseries.append(now, message.stats[key]);
}
} else if (message &&
message.message === 'tfrpc' &&
@ -652,27 +819,8 @@ function _receive_websocket_message(message) {
}
}
function keyEvent(event) {
send({
event: "key",
type: event.type,
which: event.which,
keyCode: event.keyCode,
charCode: event.charCode,
character: String.fromCharCode(event.keyCode || event.which),
altKey: event.altKey,
});
}
function setStatusMessage(message, color) {
let node = document.getElementById("status");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
if (message) {
node.appendChild(document.createTextNode(message));
node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor));
}
document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color};
}
function send(value) {
@ -685,39 +833,6 @@ function send(value) {
}
}
function updateLogin() {
let login = document.getElementById("login");
while (login.firstChild) {
login.removeChild(login.firstChild);
}
let a = document.createElement("a");
if (gCredentials && gCredentials.session) {
a.appendChild(document.createTextNode("logout " + gCredentials.session.name));
a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash()));
} else {
a.appendChild(document.createTextNode("login"));
a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash()));
}
login.appendChild(a);
}
function dragHover(event) {
event.stopPropagation();
event.preventDefault();
let input = document.getElementById("input");
if (event.type == "dragover") {
if (!input.classList.contains("drop")) {
input.classList.add("drop");
gOriginalInput = input.value;
input.value = "drop file to upload";
}
} else {
input.classList.remove("drop");
input.value = gOriginalInput;
}
}
function fixImage(sourceData, maxWidth, maxHeight, callback) {
let result = sourceData;
let image = new Image();
@ -747,52 +862,6 @@ function sendImage(image) {
});
}
function fileDropRead(event) {
sendImage(event.target.result);
}
function fileDrop(event) {
dragHover(event);
let done = false;
if (!done) {
let files = event.target.files || event.dataTransfer.files;
for (let i = 0; i < files.length; i++) {
let file = files[i];
if (file.type.substring(0, "image/".length) == "image/") {
let reader = new FileReader();
reader.onloadend = fileDropRead;
reader.readAsDataURL(file);
done = true;
}
}
}
if (!done) {
let html = event.dataTransfer.getData("text/html");
let match = /<img.*src="([^"]+)"/.exec(html);
if (match) {
sendImage(match[1]);
done = true;
}
}
if (!done) {
let text = event.dataTransfer.getData("text/plain");
if (text) {
send(text);
done = true;
}
}
}
function enableDragDrop() {
let body = document.body;
body.addEventListener("dragover", dragHover);
body.addEventListener("dragleave", dragHover);
body.addEventListener("drop", fileDrop);
}
function hashChange() {
send({event: 'hashChange', hash: window.location.hash});
}
@ -871,6 +940,8 @@ function connectSocket(path) {
gSocket.send(JSON.stringify({
action: "hello",
path: connect_path,
url: window.location.href,
edit_only: editing() && is_edit_only(),
api: Object.entries(k_api).map(([key, value]) => [].concat([key], value.args)),
}));
}
@ -911,27 +982,13 @@ function openFile(name) {
gEditor.focus();
}
function onFileClicked(event) {
openFile(event.target.textContent);
}
function updateFiles() {
let node = document.getElementById("files");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
for (let file of Object.keys(gFiles).sort()) {
let li = document.createElement("li");
li.onclick = onFileClicked;
li.appendChild(document.createTextNode(file));
if (file == gCurrentFile) {
li.classList.add("current");
}
if (!gFiles[file].doc.isClean(gFiles[file].generation)) {
li.classList.add("dirty");
}
node.appendChild(li);
let files = document.getElementsByTagName("tf-files-pane")[0];
if (files) {
files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
clean: gFiles[file].doc.isClean(gFiles[file].generation),
}]));
files.current = gCurrentFile;
}
gEditor.focus();
@ -966,28 +1023,14 @@ window.addEventListener("load", function() {
window.addEventListener("message", message, false);
window.addEventListener("online", connectSocket);
document.getElementById("name").value = window.location.pathname;
document.getElementById('edit_link').addEventListener('click', function(event) {
event.preventDefault();
toggleEdit();
});
document.getElementById('show_permissions_link').addEventListener('click', () => showPermissions());
document.getElementById('files_hide').addEventListener('click', () => hideFiles());
document.getElementById('files_show').addEventListener('click', () => showFiles());
document.getElementById('closeStats').addEventListener('click', () => closeStats());
document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
document.getElementById('save').addEventListener('click', () => save());
document.getElementById('icon').addEventListener('click', () => changeIcon());
document.getElementById('delete').addEventListener('click', () => deleteApp());
document.getElementById('trace_button').addEventListener('click', function(event) {
event.preventDefault();
event.preventDefault();
trace();
});
document.getElementById('stats_button').addEventListener('click', function(event) {
event.preventDefault();
toggleStats();
});
document.getElementById('new_file_button').addEventListener('click', () => newFile());
document.getElementById('remove_file_button').addEventListener('click', () => removeFile());
for (let tag of document.getElementsByTagName('a')) {
if (tag.accessKey) {
tag.classList.add('tooltip_parent');
@ -1012,7 +1055,6 @@ window.addEventListener("load", function() {
tag.appendChild(tooltip);
}
}
enableDragDrop();
connectSocket(window.location.pathname);
if (window.localStorage.getItem('editing') == '1') {
@ -1020,14 +1062,4 @@ window.addEventListener("load", function() {
} else {
closeEditor();
}
if (window.localStorage.getItem('files') == '1') {
showFiles();
} else {
hideFiles();
}
if (window.localStorage.getItem('stats') == '1') {
stats();
} else {
closeStats();
}
});

View File

@ -1,12 +1,46 @@
import * as app from './app.js';
import * as auth from './auth.js';
import * as form from './form.js';
import * as http from './http.js';
import * as httpd from './httpd.js';
let gProcessIndex = 0;
let gProcesses = {};
let gStatsTimer = false;
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'},
];
let k_static_files = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
];
const k_global_settings = {
index: {
type: 'string',
@ -33,6 +67,21 @@ const k_global_settings = {
default_value: undefined,
description: 'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
},
fetch_hosts: {
type: 'string',
default_value: undefined,
description: 'Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.',
},
blob_fetch_age_seconds: {
type: 'integer',
default_value: (platform() == 'android' ? 0.5 * 365 * 24 * 60 * 60 : undefined),
description: 'Only blobs mentioned more recently than this age will be automatically fetched.',
},
blob_expire_age_seconds: {
type: 'integer',
default_value: (platform() == 'android' ? 1.0 * 365 * 24 * 60 * 60 : undefined),
description: 'Blobs older than this will be automatically deleted.',
},
};
let gGlobalSettings = {
@ -46,8 +95,8 @@ function printError(out, error) {
out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
out.print(error.stackTrace);
} else {
for (let i in error) {
out.print(i);
for (let [k, v] of Object.entries(error)) {
out.print(k, v);
}
out.print(error.toString());
}
@ -159,7 +208,9 @@ async function getProcessBlob(blobId, key, options) {
process.credentials = options.credentials || {};
process.task = new Task();
process.eventHandlers = {};
process.app = new app.App();
if (!options?.script || options?.script === 'app.js') {
process.app = new app.App();
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = options.timeout;
@ -243,7 +294,7 @@ async function getProcessBlob(blobId, key, options) {
throw Error(`Permission denied: ${permission}.`);
}
} else {
} else if (process.app) {
return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
if (value == 'allow') {
storePermission(user, options.packageOwner, options.packageName, permission, true);
@ -260,8 +311,11 @@ async function getProcessBlob(blobId, key, options) {
}
throw Error(`Permission denied: ${permission}.`);
});
} else {
throw Error(`Permission denied: ${permission}.`);
}
},
url: options?.url,
}
};
if (process.credentials?.permissions?.administration) {
@ -284,7 +338,7 @@ async function getProcessBlob(blobId, key, options) {
print('Done.');
};
imports.core.deleteUser = function(user) {
return imports.core.permissionTest('delete_user').then(function() {
return Promise.resolve(imports.core.permissionTest('delete_user')).then(function() {
let db = new Database('auth');
db.remove('user:' + user);
@ -310,14 +364,23 @@ async function getProcessBlob(blobId, key, options) {
imports.app[api[0]] = process.app.makeFunction(api);
}
}
for (let [name, f] of Object.entries(options?.imports || {})) {
imports[name] = f;
}
process.task.onPrint = function(args) {
imports.app.print(...args);
if (imports.app) {
imports.app.print(...args);
}
};
process.task.onError = function(error) {
try {
process.app.makeFunction(['error'])(error);
} catch(e) {
print(e);
if (process.app) {
process.app.makeFunction(['error'])(error);
} else {
printError({print: print}, error);
}
} catch (e) {
printError({print: print}, error);
}
};
imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
@ -339,7 +402,7 @@ async function getProcessBlob(blobId, key, options) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return imports.core.permissionTest('ssb_append').then(function() {
return Promise.resolve(imports.core.permissionTest('ssb_append')).then(function() {
return ssb.appendMessageWithIdentity(process.credentials.session.name, id, message);
});
}
@ -358,6 +421,9 @@ async function getProcessBlob(blobId, key, options) {
return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
}
};
imports.fetch = function(url, options) {
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
}
if (process.credentials &&
process.credentials.session &&
@ -396,7 +462,7 @@ async function getProcessBlob(blobId, key, options) {
try {
let appObject = JSON.parse(appSource);
if (appObject.type == "tildefriends-app") {
appSourceName = 'app.js';
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await getBlobOrContent(id);
appSource = utf8Decode(blob);
@ -409,19 +475,23 @@ async function getProcessBlob(blobId, key, options) {
printError({print: print}, e);
}
broadcastEvent('onSessionBegin', [getUser(process, process)]);
resolveReady(process);
if (process.app) {
process.app.send({action: "ready"});
process.app.send({action: "ready", version: version()});
process.sendPermissions();
}
await process.task.execute({name: appSourceName, source: appSource});
resolveReady(process);
} catch (error) {
if (process?.task?.onError) {
process.task.onError(error);
if (process.app) {
if (process?.task?.onError) {
process.task.onError(error);
} else {
printError({print: print}, error);
}
} else {
printError({print: print}, error);
}
rejectReady();
rejectReady(error);
}
}
return process;
@ -436,20 +506,11 @@ function setGlobalSettings(settings) {
}
}
let kStaticFiles = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
];
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) {
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
return;
}
}
@ -458,16 +519,16 @@ function startsWithBytes(data, bytes) {
}
async function staticFileHandler(request, response, blobId, uri) {
for (let i in kStaticFiles) {
if (uri === kStaticFiles[i].uri) {
let path = kStaticFiles[i].path || uri.substring(1);
let type = kStaticFiles[i].type || guessType(path);
for (let i in k_static_files) {
if (uri === k_static_files[i].uri) {
let path = k_static_files[i].path || uri.substring(1);
let type = k_static_files[i].type || guessTypeFromName(path);
let stat = await File.stat('core/' + path);
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile('core/' + path);
@ -477,7 +538,7 @@ async function staticFileHandler(request, response, blobId, uri) {
'Content-Length': data.byteLength,
'etag': '"' + id + '"',
},
kStaticFiles[i].headers || {}));
k_static_files[i].headers || {}));
response.end(data);
}
return;
@ -488,14 +549,6 @@ async function staticFileHandler(request, response, blobId, uri) {
response.end("File not found");
}
const k_mime_types = {
'json': 'text/json',
'js': 'text/javascript',
'html': 'text/html',
'css': 'text/css',
'map': 'application/json',
};
async function staticDirectoryHandler(request, response, directory, uri) {
let filename = uri || 'index.html';
if (filename.indexOf('..') != -1) {
@ -509,7 +562,7 @@ async function staticDirectoryHandler(request, response, directory, uri) {
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile(directory + filename);
@ -537,40 +590,25 @@ async function wellKnownHandler(request, response, path) {
}
}
function sendData(response, data, type, headers) {
if (data) {
if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
response.writeHead(200, Object.assign({"Content-Type": "image/jpeg", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
response.writeHead(200, Object.assign({"Content-Type": "image/png", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
response.writeHead(200, Object.assign({"Content-Type": "image/gif", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
response.writeHead(200, Object.assign({"Content-Type": "image/webp", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x3c, 0x73, 0x76, 0x67])) {
response.writeHead(200, Object.assign({"Content-Type": "image/svg+xml", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
response.writeHead(200, Object.assign({"Content-Type": "audio/mpeg", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
response.writeHead(200, Object.assign({"Content-Type": "video/mp4", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else {
response.writeHead(200, Object.assign({"Content-Type": type || "application/binary", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
function guessTypeFromName(path) {
let extension = path.split('.').pop();
return k_mime_types[extension];
}
function guessTypeFromMagicBytes(data) {
for (let magic of k_magic_bytes) {
if (startsWithBytes(data, magic.bytes)) {
return magic.type;
}
}
}
function sendData(response, data, type, headers, status_code) {
if (data) {
response.writeHead(status_code ?? 200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else {
response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
response.writeHead(status_code ?? 404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
response.end("File not found");
}
}
@ -585,35 +623,58 @@ async function getBlobOrContent(id) {
}
}
function guessType(path) {
const k_extension_to_type = {
'css': 'text/css',
'html': 'text/html',
'js': 'text/javascript',
'svg': 'image/svg+xml',
};
let extension = path.split('.').pop();
return k_extension_to_type[extension];
let g_handler_index = 0;
async function useAppHandler(response, handler_blob_id, path, query, headers, packageOwner, packageName) {
print('useAppHandler', packageOwner, packageName);
let do_resolve;
let promise = new Promise(async function(resolve, reject) {
do_resolve = resolve;
});
let process;
let result;
try {
process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, {
script: 'handler.js',
imports: {
request: {
path: path,
query: query,
},
respond: do_resolve,
},
credentials: auth.query(headers),
packageOwner: packageOwner,
packageName: packageName,
});
await process.ready;
result = await promise;
} finally {
if (process?.task) {
await process.task.kill();
}
}
return result;
}
async function blobHandler(request, response, blobId, uri) {
for (let i in kStaticFiles) {
if (uri === kStaticFiles[i].uri && kStaticFiles[i].path) {
let stat = await File.stat('core/' + kStaticFiles[i].path);
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, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile('core/' + kStaticFiles[i].path);
let data = await File.readFile('core/' + k_static_files[i].path);
response.writeHead(200, Object.assign(
{
'Content-Type': kStaticFiles[i].type,
'Content-Type': k_static_files[i].type,
'Content-Length': data.byteLength,
'etag': '"' + id + '"',
},
kStaticFiles[i].headers || {}));
k_static_files[i].headers || {}));
response.end(data);
}
return;
@ -631,7 +692,9 @@ async function blobHandler(request, response, blobId, uri) {
let data;
let match;
let query = form.decodeForm(request.query);
let headers = {};
let headers = {
'Content-Security-Policy': 'sandbox',
};
if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
}
@ -639,6 +702,7 @@ async function blobHandler(request, response, blobId, uri) {
let id = await new Database(match[1]).get('path:' + match[2]);
if (id) {
if (request.headers['if-none-match'] === '"' + id + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
@ -651,6 +715,7 @@ async function blobHandler(request, response, blobId, uri) {
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
@ -659,6 +724,7 @@ async function blobHandler(request, response, blobId, uri) {
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
@ -738,43 +804,58 @@ async function blobHandler(request, response, blobId, uri) {
response.end('OK');
} else {
let data;
let type;
let headers;
let match;
let id;
let app_id = blobId;
let packageOwner;
let packageName;
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
packageOwner = match[1];
packageName = match[2];
let db = new Database(match[1]);
let id = await db.get('path:' + match[2]);
if (id) {
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
headers = {
'Access-Control-Allow-Origin': '*',
};
response.writeHead(304, headers);
response.end();
} else {
data = utf8Decode(await getBlobOrContent(id));
let appObject = JSON.parse(data);
data = appObject.files[uri.substring(1)];
data = await getBlobOrContent(data);
type = guessType(uri);
headers = {
'ETag': '"' + id + '"',
'Access-Control-Allow-Origin': '*',
};
sendData(response, data, type, headers);
}
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 answer;
try {
answer = await useAppHandler(response, app_id, uri.substring(1), request.query ? form.decodeForm(request.query) : undefined, request.headers, packageOwner, packageName);
} catch (error) {
data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`);
response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length});
response.end(data);
return;
}
if (answer && typeof answer.data == 'string') {
answer.data = utf8Encode(answer.data);
}
sendData(response, answer?.data, answer?.content_type, Object.assign(answer?.headers ?? {}, {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
}), answer.status_code);
} else if (id) {
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
let headers = {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
'Content-Length': '0',
};
response.writeHead(304, headers);
response.end();
} else {
let headers = {
'ETag': '"' + id + '"',
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
};
data = await getBlobOrContent(id);
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
sendData(response, data, type, headers);
}
} else {
data = utf8Decode(await getBlobOrContent(blobId));
let appObject = JSON.parse(data);
data = appObject.files[uri.substring(1)];
data = await getBlobOrContent(data);
headers = {
'Access-Control-Allow-Origin': '*',
};
sendData(response, data, type, headers);
sendData(response, data, undefined, {});
}
}
}
@ -826,6 +907,16 @@ function enableStats(process, enabled) {
}
}
function stringResponse(response, data) {
let bytes = utf8Encode(data);
response.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": bytes.byteLength.toString(),
"Access-Control-Allow-Origin": "*",
});
return response.end(bytes);
}
loadSettings().then(function() {
httpd.all("/login", auth.handler);
httpd.all("", function(request, response) {
@ -837,40 +928,28 @@ loadSettings().then(function() {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
return staticFileHandler(request, response, null, match[1]);
} else if (match = /^\/static\/lit\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/lit/', match[1]);
} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/speedscope/', match[1]);
} else if (match = /^\/split\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/split/', match[1]);
} else if (match = /^\/smoothie\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/smoothie/', match[1]);
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
return staticFileHandler(request, response, null, match[1]);
} else if (request.uri == "/robots.txt") {
return staticFileHandler(request, response, null, request.uri);
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/trace$/.exec(request.uri)) {
let data = trace();
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
return stringResponse(response, trace());
} else if (match = /^\/disconnections$/.exec(request.uri)) {
let data = utf8Encode(JSON.stringify(disconnectionsDebug(), null, 2));
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.byteLength.toString()});
return response.end(data);
return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
} else if (match = /^\/debug$/.exec(request.uri)) {
let data = JSON.stringify(getDebug(), null, 2);
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
return stringResponse(response, JSON.stringify(getDebug(), null, 2));
} else if (match = /^\/hitches$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getHitches(), null, 2));
} else if (match = /^\/mem$/.exec(request.uri)) {
let data = JSON.stringify(getAllocations(), null, 2);
response.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": data.length.toString(),
"Access-Control-Allow-Origin": "*",
});
return response.end(data);
} else if (request.uri == "/robots.txt") {
return blobHandler(request, response, null, request.uri);
return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
return wellKnownHandler(request, response, match[1]);
} else {

86
core/http.js Normal file
View File

@ -0,0 +1,86 @@
function parseUrl(url) {
// XXX: Hack.
let match = url.match(new RegExp("(\\w+)://([^/:]+)(?::(\\d+))?(.*)"));
return {
protocol: match[1],
host: match[2],
path: match[4],
port: match[3] ? parseInt(match[3]) : match[1] == "http" ? 80 : 443,
};
}
function parseResponse(data) {
let firstLine;
let headers = {};
while (true) {
let endLine = data.indexOf('\r\n');
let line = data.substring(0, endLine);
data = data.substring(endLine + 2);
if (!line.length) {
break;
} else if (!firstLine) {
firstLine = line;
} else {
let colon = line.indexOf(":");
headers[line.substring(colon)] = line.substring(colon + 1);
}
}
return {body: data};
}
export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url);
return new Promise(function(resolve, reject) {
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
}
let socket = new Socket();
let buffer = new Uint8Array(0);
return socket.connect(parsed.host, parsed.port).then(function() {
socket.read(function(data) {
if (data && data.length) {
let newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer, 0);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
} else {
let result = parseHttpResponse(buffer);
if (!result) {
reject(new Exception('Parse failed.'));
}
if (typeof result == 'number') {
if (result == -2) {
reject('Incomplete request.');
} else {
reject('Bad request.');
}
} else if (typeof result == 'object') {
resolve({
body: buffer.slice(result.bytes_parsed),
status: result.status,
message: result.message,
headers: result.headers,
});
} else {
reject(new Exception('Unexpected parse result.'));
}
resolve(parseResponse(utf8Decode(buffer)));
}
});
if (parsed.port == 443) {
return socket.startTls();
}
}).then(function() {
let body = typeof options?.body == 'string' ? utf8Encode(options.body) : (options.body || new Uint8Array(0));
let headers = utf8Encode(`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`);
let fullRequest = new Uint8Array(headers.length + body.length);
fullRequest.set(headers, 0);
fullRequest.set(body, headers.length);
socket.write(fullRequest);
}).catch(function(error) {
reject(error);
});
});
}

View File

@ -4,7 +4,7 @@ let gHandlers = [];
let gSocketHandlers = [];
let gBadRequests = {};
const kRequestTimeout = 15000;
const kRequestTimeout = 5000;
const kStallTimeout = 60000;
function logError(error) {
@ -395,41 +395,10 @@ function handleConnection(client) {
let parsing_header = true;
let bodyToRead = -1;
let body;
let requestCount = -1;
let readCount = 0;
let isWebsocket = false;
function resetTimeout(requestIndex) {
if (isWebsocket) {
return;
}
if (bodyToRead == -1) {
setTimeout(function() {
if (requestCount == requestIndex) {
client.info = 'timed out';
if (requestCount == 0) {
badRequest(client, 'Timed out waiting for request.');
} else {
client.close();
}
}
}, kRequestTimeout);
} else {
let lastReadCount = readCount;
setTimeout(function() {
if (readCount == lastReadCount) {
client.info = 'stalled';
if (requestCount == 0) {
badRequest(client, 'Request stalled.');
} else {
client.close();
}
}
}, kStallTimeout);
}
}
resetTimeout(++requestCount);
client.setActivityTimeout(kRequestTimeout);
function reset() {
request = undefined;
@ -438,7 +407,7 @@ function handleConnection(client) {
bodyToRead = -1;
body = undefined;
client.info = 'reset';
resetTimeout(++requestCount);
client.setActivityTimeout(kRequestTimeout);
}
function finish() {
@ -463,9 +432,6 @@ function handleConnection(client) {
client.read(function(data) {
readCount++;
if (data) {
if (bodyToRead != -1 && !isWebsocket) {
resetTimeout(requestCount);
}
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
newBuffer.set(inputBuffer, 0);
newBuffer.set(data, inputBuffer.length);
@ -473,7 +439,7 @@ function handleConnection(client) {
if (parsing_header)
{
let result = parseHttp(inputBuffer, inputBuffer.length - data.length);
let result = parseHttpRequest(inputBuffer, inputBuffer.length - data.length);
if (result) {
if (typeof result === 'number') {
if (result == -2) {
@ -483,6 +449,7 @@ function handleConnection(client) {
return;
}
} else if (typeof result === 'object') {
client.setActivityTimeout(kStallTimeout);
request = [
result.method,
result.path,
@ -509,7 +476,6 @@ function handleConnection(client) {
}
body = new Uint8Array(bodyToRead);
client.info = 'waiting for body';
resetTimeout(requestCount);
} else if (headers["connection"]
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
&& headers["upgrade"]
@ -520,7 +486,7 @@ function handleConnection(client) {
let response = new Response(requestObject, client);
handleWebSocketRequest(requestObject, response, client);
/* Prevent the timeout from disconnecting us. */
requestCount++;
client.setActivityTimeout();
} else {
finish();
}
@ -552,7 +518,7 @@ function handleConnection(client) {
}
let kBacklog = 8;
let kHost = "0.0.0.0"
let kHost = '::';
let socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function(port) {

View File

@ -7,26 +7,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="display: flex; flex-flow: column">
<div class="navigation">
<span>😎</span>
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff">Tilde Friends</a>
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" data-tip="Toggle the app editor." href="#" id="edit_link">edit</a>
<a accesskey="p" data-tip="View and change permissions." href="#" id="show_permissions_link">🎛️</a>
<span id="status"></span>
<span id="requests"></span>
<span id="permissions_settings"></span>
<span id="permissions"></span>
<span id="login"></span>
</div>
<tf-navigation></tf-navigation>
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
<div id="statsPane" class="vbox" style="display: none; flex 1 1">
<div class="hbox">
<input type="button" id="closeStats" name="closeStats" value="Close">
</div>
<div id="graphs" class="vbox" style="height: 100%"></div>
</div>
<div id="editPane" class="vbox" style="display: none">
<div id="editPane" class="vbox" style="flex: 1 1; display: none">
<div class="navigation hbox">
<input type="button" id="closeEditor" name="closeEditor" value="Close">
<input type="button" id="save" name="save" value="Save">
@ -34,22 +17,9 @@
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
<input type="button" id="delete" name="delete" value="Delete">
<input type="button" id="trace_button" value="Trace">
<input type="button" id="stats_button" value="Stats">
</div>
<div class="hbox" style="height: 100%">
<div id="filesPane">
<div class="hbox">
<span id="files_header">Files</span>
<span id="files_hide">«</span>
<span id="files_show">»</span>
</div>
<div id="files_content">
<ul id="files"></ul>
<br>
<div><button id="new_file_button">New File</button></div>
<div><button id="remove_file_button">Remove File</button></div>
</div>
</div>
<tf-files-pane></tf-files-pane>
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
<div style="flex: 1 1 50%; position: relative">
<textarea id="editor" class="main"></textarea>
@ -57,12 +27,11 @@
</div>
</div>
</div>
<div id="viewPane" class="vbox" style="flex: 1 0; overflow: auto">
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
<div id="viewPane" class="vbox" style="flex: 1 1; overflow: auto">
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
</div>
</div>
<script src="/split/split.min.js"></script>
<script src="/smoothie/smoothie.js"></script>
<script>window.litDisableBundleWarning = true;</script>
<script src="/static/client.js" type="module"></script>
</body>
</html>

View File

@ -15,11 +15,6 @@ body {
margin: 0;
}
.navigation {
height: auto;
margin: 4px;
}
a:link {
color: #268bd2;
}
@ -69,11 +64,6 @@ a:active {
color: #eee8d5;
}
#input.drop {
border: 2px solid;
color: #cb4b16;
}
.CodeMirror {
height: 100%;
padding: 0;
@ -110,33 +100,6 @@ a:active {
float: right;
}
#auth_greeting {
text-align: center;
}
#auth {
display: flex;
flex-flow: row;
align-content: center;
align-items: center;
text-align: center;
justify-content: center;
}
#auth_login {
flex: 0 1 auto;
text-align: right;
}
.auth_or {
flex: 0 1 auto;
padding: 1em;
}
#auth_guest {
flex: 0 1 auto;
}
.notice {
color: #cb4b16;
margin: 1em;
@ -162,70 +125,6 @@ a:active {
.cyan { color: #2aa198; }
.green { color: #859900; }
#files_header {
font-weight: bold;
text-align: center;
flex: 1;
}
#files_hide {
font-weight: bold;
width: 100%;
right: 0;
flex: 0;
padding: 0.25em;
cursor: pointer;
}
#files_show {
display: none;
padding: 0.25em;
cursor: pointer;
}
#filesPane {
flex: 1 1 10%;
}
#filesPane.collapsed {
flex: 0;
}
.collapsed #files_header {
display: none;
}
.collapsed #files_content {
display: none;
}
.collapsed #files_hide {
display: none;
}
.collapsed #files_show {
display: block;
}
#files {
list-style-type: none;
margin: 0;
padding: 0;
}
#files > li {
padding: 0.5em;
}
#files > li.current {
font-weight: bold;
background-color: #2aa198;
}
#files > li.dirty::after {
content: '*';
}
.tooltip {
position: absolute;
z-index: 1;
@ -254,8 +153,7 @@ kbd {
white-space: nowrap;
}
#permissions, #permissions_settings {
visibility: hidden;
.permissions {
position: absolute;
display: block;
top: 0;

View File

@ -39,9 +39,9 @@ function send(response) {
function call_rpc(message) {
if (message && message.message === 'tfrpc') {
let method = g_api[message.method];
let id = message.id;
if (message.method) {
let method = g_api[message.method];
if (method) {
try {
Promise.resolve(method(...message.params)).then(function(result) {
@ -53,7 +53,7 @@ function call_rpc(message) {
send({message: 'tfrpc', id: id, error: error});
}
} else {
throw new Error(message.method + ' not found.');
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`});
}
} else if (message.error !== undefined) {
if (g_calls[id]) {

View File

@ -1 +1 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h&&(s=n.getLineNumber(i),h=i)),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id&&f.setAttribute("annotation-id",u.id))}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h)&&(s=n.getLineNumber(i),h=i),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id)&&f.setAttribute("annotation-id",u.id)}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});

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 +1 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen&&l.select()),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen)&&l.select(),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});

View File

@ -1 +1 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol())&&(e.inTag+=" "),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});

View File

@ -0,0 +1 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(a){"use strict";a.registerHelper("lint","javascript",function(e,r){if(!window.JSHINT)return window.console&&window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run."),[];if(r.indent||(r.indent=1),JSHINT(e,r,r.globals),e=JSHINT.data().errors,r=[],e)for(var n=e,o=r,i=0;i<n.length;i++){var t,d,s,c=n[i];c&&(c.line<=0?window.console&&window.console.warn("Cannot display JSHint error (invalid line "+c.line+")",c):(t=c.character-1,d=1+t,c.evidence&&-1<(s=c.evidence.substring(t).search(/.\b/))&&(d+=s),s={message:c.reason,severity:c.code&&c.code.startsWith("W")?"warning":"error",from:a.Pos(c.line-1,t),to:a.Pos(c.line-1,d)},o.push(s)))}return r})});

File diff suppressed because one or more lines are too long

32076
deps/codemirror/jshint.js vendored Normal file

File diff suppressed because one or more lines are too long

80
deps/codemirror/lint.css vendored Normal file
View File

@ -0,0 +1,80 @@
/* The lint marker gutter */
.CodeMirror-lint-markers {
width: 16px;
}
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-mark-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
}
.CodeMirror-lint-marker {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
position: relative;
}
.CodeMirror-lint-message {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right bottom;
width: 100%; height: 100%;
}
.CodeMirror-lint-line-error {
background-color: rgba(183, 76, 81, 0.08);
}
.CodeMirror-lint-line-warning {
background-color: rgba(255, 211, 0, 0.1);
}

292
deps/codemirror/lint.js vendored Normal file
View File

@ -0,0 +1,292 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
var GUTTER_ID = "CodeMirror-lint-markers";
var LINT_LINE_ID = "CodeMirror-lint-line-";
function showTooltip(cm, e, content) {
var tt = document.createElement("div");
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
tt.appendChild(content.cloneNode(true));
if (cm.state.lint.options.selfContain)
cm.getWrapperElement().appendChild(tt);
else
document.body.appendChild(tt);
function position(e) {
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
tt.style.left = (e.clientX + 5) + "px";
}
CodeMirror.on(document, "mousemove", position);
position(e);
if (tt.style.opacity != null) tt.style.opacity = 1;
return tt;
}
function rm(elt) {
if (elt.parentNode) elt.parentNode.removeChild(elt);
}
function hideTooltip(tt) {
if (!tt.parentNode) return;
if (tt.style.opacity == null) rm(tt);
tt.style.opacity = 0;
setTimeout(function() { rm(tt); }, 600);
}
function showTooltipFor(cm, e, content, node) {
var tooltip = showTooltip(cm, e, content);
function hide() {
CodeMirror.off(node, "mouseout", hide);
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
}
var poll = setInterval(function() {
if (tooltip) for (var n = node;; n = n.parentNode) {
if (n && n.nodeType == 11) n = n.host;
if (n == document.body) return;
if (!n) { hide(); break; }
}
if (!tooltip) return clearInterval(poll);
}, 400);
CodeMirror.on(node, "mouseout", hide);
}
function LintState(cm, conf, hasGutter) {
this.marked = [];
if (conf instanceof Function) conf = {getAnnotations: conf};
if (!conf || conf === true) conf = {};
this.options = {};
this.linterOptions = conf.options || {};
for (var prop in defaults) this.options[prop] = defaults[prop];
for (var prop in conf) {
if (defaults.hasOwnProperty(prop)) {
if (conf[prop] != null) this.options[prop] = conf[prop];
} else if (!conf.options) {
this.linterOptions[prop] = conf[prop];
}
}
this.timeout = null;
this.hasGutter = hasGutter;
this.onMouseOver = function(e) { onMouseOver(cm, e); };
this.waitingFor = 0
}
var defaults = {
highlightLines: false,
tooltips: true,
delay: 500,
lintOnChange: true,
getAnnotations: null,
async: false,
selfContain: null,
formatAnnotation: null,
onUpdateLinting: null
}
function clearMarks(cm) {
var state = cm.state.lint;
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
if (state.options.highlightLines) clearErrorLines(cm);
for (var i = 0; i < state.marked.length; ++i)
state.marked[i].clear();
state.marked.length = 0;
}
function clearErrorLines(cm) {
cm.eachLine(function(line) {
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
if (has) cm.removeLineClass(line, "wrap", has[0]);
})
}
function makeMarker(cm, labels, severity, multiple, tooltips) {
var marker = document.createElement("div"), inner = marker;
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
if (multiple) {
inner = marker.appendChild(document.createElement("div"));
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
}
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
showTooltipFor(cm, e, labels, inner);
});
return marker;
}
function getMaxSeverity(a, b) {
if (a == "error") return a;
else return b;
}
function groupByLine(annotations) {
var lines = [];
for (var i = 0; i < annotations.length; ++i) {
var ann = annotations[i], line = ann.from.line;
(lines[line] || (lines[line] = [])).push(ann);
}
return lines;
}
function annotationTooltip(ann) {
var severity = ann.severity;
if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
if (typeof ann.messageHTML != 'undefined') {
tip.innerHTML = ann.messageHTML;
} else {
tip.appendChild(document.createTextNode(ann.message));
}
return tip;
}
function lintAsync(cm, getAnnotations) {
var state = cm.state.lint
var id = ++state.waitingFor
function abort() {
id = -1
cm.off("change", abort)
}
cm.on("change", abort)
getAnnotations(cm.getValue(), function(annotations, arg2) {
cm.off("change", abort)
if (state.waitingFor != id) return
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
cm.operation(function() {updateLinting(cm, annotations)})
}, state.linterOptions, cm);
}
function startLinting(cm) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
/*
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
*/
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
if (!getAnnotations) return;
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations)
} else {
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
cm.operation(function() {updateLinting(cm, issues)})
});
else cm.operation(function() {updateLinting(cm, annotations)})
}
}
function updateLinting(cm, annotationsNotSorted) {
var state = cm.state.lint;
if (!state) return;
var options = state.options;
clearMarks(cm);
var annotations = groupByLine(annotationsNotSorted);
for (var line = 0; line < annotations.length; ++line) {
var anns = annotations[line];
if (!anns) continue;
// filter out duplicate messages
var message = [];
anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) });
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
__annotation: ann
}));
}
// use original annotations[line] to show multiple messages
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1,
options.tooltips));
if (options.highlightLines)
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
}
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
}
function onChange(cm) {
var state = cm.state.lint;
if (!state) return;
clearTimeout(state.timeout);
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
}
function popupTooltips(cm, annotations, e) {
var target = e.target || e.srcElement;
var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(cm, e, tooltip, target);
}
function onMouseOver(cm, e) {
var target = e.target || e.srcElement;
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation;
if (ann) annotations.push(ann);
}
if (annotations.length) popupTooltips(cm, annotations, e);
}
CodeMirror.defineOption("lint", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearMarks(cm);
if (cm.state.lint.options.lintOnChange !== false)
cm.off("change", onChange);
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
clearTimeout(cm.state.lint.timeout);
delete cm.state.lint;
}
if (val) {
var gutters = cm.getOption("gutters"), hasLintGutter = false;
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
if (state.options.lintOnChange)
cm.on("change", onChange);
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);
}
});
CodeMirror.defineExtension("performLint", function() {
startLinting(this);
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
deps/codemirror/update.sh vendored Executable file
View File

@ -0,0 +1,22 @@
LINKS="
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/dialog/dialog.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/dialog/dialog.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/edit/trailingspace.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/lint/javascript-lint.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/scroll/annotatescrollbar.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/matchesonscrollbar.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/matchesonscrollbar.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/search.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/addon/search/searchcursor.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.css
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/codemirror.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/css/css.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/htmlmixed/htmlmixed.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/javascript/javascript.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/mode/xml/xml.min.js
https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.14/theme/base16-dark.min.css
"
for link in $LINKS; do
wget $link -O `basename $link`
done

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
name: CI-docs
on:
pull_request:
paths:
- 'docs/**'
- '!docs/code/**'
- '.github/workflows/CI-docs.yml'
jobs:
docs-src:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.9'
cache: 'pip' # caching pip dependencies
- run: pip install -r docs/requirements.txt
- name: html
run: |
make -C docs html
- name: linkcheck
run: |
make -C docs linkcheck

View File

@ -14,6 +14,19 @@ on:
- master
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: configure
run: |
./autogen.sh
mkdir build
(cd build && ../configure)
- name: distcheck
run: |
make -C build distcheck
build-android:
runs-on: ubuntu-latest
container: reactnativecommunity/react-native-android:2020-5-20
@ -33,14 +46,14 @@ jobs:
ls -lh build
build-macos:
runs-on: macos-10.15
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Envinfo
run: npx envinfo
- name: Setup
run: |
brew install ninja
brew install ninja automake libtool
- name: Configure
run: |
mkdir build
@ -59,9 +72,16 @@ jobs:
- name: Test
run: |
cd build && ctest -V
- name: Autotools configure
if: always()
run: |
./autogen.sh
mkdir build-auto
(cd build-auto && ../configure)
make -C build-auto -j4
build-ios:
runs-on: macos-10.15
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Configure
@ -94,7 +114,6 @@ jobs:
- {target: mips64, toolchain: gcc-mips64-linux-gnuabi64, cc: mips64-linux-gnuabi64-gcc, qemu: qemu-mips64-static }
- {target: mipsel, toolchain: gcc-mipsel-linux-gnu, cc: mipsel-linux-gnu-gcc, qemu: qemu-mipsel-static }
- {target: mips64el,toolchain: gcc-mips64el-linux-gnuabi64, cc: mips64el-linux-gnuabi64-gcc,qemu: qemu-mips64el-static }
- {target: alpha, toolchain: gcc-alpha-linux-gnu, cc: alpha-linux-gnu-gcc, qemu: qemu-alpha-static }
- {target: arm (u64 slots), toolchain: gcc-arm-linux-gnueabi, cc: arm-linux-gnueabi-gcc, qemu: qemu-arm-static}
- {target: aarch64 (u64 slots), toolchain: gcc-aarch64-linux-gnu, cc: aarch64-linux-gnu-gcc, qemu: qemu-aarch64-static}
- {target: ppc (u64 slots), toolchain: gcc-powerpc-linux-gnu, cc: powerpc-linux-gnu-gcc, qemu: qemu-ppc-static}
@ -106,7 +125,7 @@ jobs:
# this ensure install latest qemu on ubuntu, apt get version is old
env:
QEMU_SRC: "http://archive.ubuntu.com/ubuntu/pool/universe/q/qemu"
QEMU_VER: "qemu-user-static_4\\.2-.*_amd64.deb$"
QEMU_VER: "qemu-user-static_7\\.0+dfsg-.*_amd64.deb$"
run: |
DEB=`curl -s $QEMU_SRC/ | grep -o -E 'href="([^"#]+)"' | cut -d'"' -f2 | grep $QEMU_VER | tail -1`
wget $QEMU_SRC/$DEB

View File

@ -16,7 +16,7 @@ on:
jobs:
build-windows:
runs-on: windows-${{ matrix.config.server }}
name: build-${{ matrix.config.toolchain}}-${{ matrix.config.arch}}
name: build-${{ join(matrix.config.*, '-') }}
strategy:
fail-fast: false
matrix:
@ -25,27 +25,98 @@ jobs:
- {toolchain: Visual Studio 16 2019, arch: x64, server: 2019}
- {toolchain: Visual Studio 17 2022, arch: Win32, server: 2022}
- {toolchain: Visual Studio 17 2022, arch: x64, server: 2022}
- {toolchain: Visual Studio 17 2022, arch: x64, server: 2022, config: ASAN}
steps:
- uses: actions/checkout@v2
- name: Envinfo
run: npx envinfo
- name: Build
shell: cmd
run: |
mkdir -p build
cd build
cmake .. -DBUILD_TESTING=ON -G "${{ matrix.config.toolchain }}" -A ${{ matrix.config.arch }}
cmake --build .
run:
cmake -S . -B build -DBUILD_TESTING=ON
-G "${{ matrix.config.toolchain }}" -A ${{ matrix.config.arch }}
${{ matrix.config.config == 'ASAN' && '-DASAN=on -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded' || '' }}
cmake --build build --config RelWithDebInfo
ls -l build
- name: platform_output
shell: cmd
run: |
build\\Debug\\uv_run_tests.exe platform_output
run:
build\\RelWithDebInfo\\uv_run_tests.exe platform_output
- name: platform_output_a
shell: cmd
run:
build\\RelWithDebInfo\\uv_run_tests_a.exe platform_output
- name: Test
# only valid with libuv-master with the fix for
# https://github.com/libuv/leps/blob/master/005-windows-handles-not-fd.md
if: ${{ matrix.config.config != 'ASAN' }}
shell: cmd
run:
cd build
ctest -C RelWithDebInfo -V
- name: Test only static
if: ${{ matrix.config.config == 'ASAN' }}
shell: cmd
run:
build\\RelWithDebInfo\\uv_run_tests_a.exe
build-mingw:
runs-on: ubuntu-latest
name: build-mingw-${{ matrix.config.arch }}
strategy:
fail-fast: false
matrix:
config:
- {arch: i686, server: 2022, libgcc: dw2 }
- {arch: x86_64, server: 2022, libgcc: seh }
steps:
- uses: actions/checkout@v3
- name: Install mingw32 environment
run: |
build\\Debug\\uv_run_tests_a.exe platform_output
sudo apt update
sudo apt install mingw-w64 ninja-build -y
- name: Build
run: |
cmake -S . -B build -G Ninja -DHOST_ARCH=${{ matrix.config.arch }} -DBUILD_TESTING=ON -DCMAKE_TOOLCHAIN_FILE=cmake-toolchains/cross-mingw32.cmake
cmake --build build
cmake --install build --prefix "`pwd`/build/usr"
mkdir -p build/usr/test build/usr/bin
cp -av test/fixtures build/usr/test
cp -av build/uv_run_tests_a.exe build/uv_run_tests.exe \
`${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libgcc_s_${{ matrix.config.libgcc }}-1.dll` \
`${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libwinpthread-1.dll` \
`${{ matrix.config.arch }}-w64-mingw32-gcc -print-file-name=libatomic-1.dll` \
build/usr/bin
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: mingw-${{ matrix.config.arch }}
path: build/usr/**/*
retention-days: 2
test-mingw:
runs-on: windows-${{ matrix.config.server }}
name: test-mingw-${{ matrix.config.arch }}
needs: build-mingw
strategy:
fail-fast: false
matrix:
config:
- {arch: i686, server: 2022}
- {arch: x86_64, server: 2022}
steps:
- name: Download build artifacts
uses: actions/download-artifact@v2
with:
name: mingw-${{ matrix.config.arch }}
- name: Test
shell: cmd
run: |
cd build
ctest -C Debug -V
bin\uv_run_tests_a.exe
- name: Test
shell: cmd
run: |
bin\uv_run_tests.exe

View File

@ -14,7 +14,7 @@ on:
jobs:
sanitizers:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Setup
@ -22,15 +22,7 @@ jobs:
sudo apt-get install ninja-build
- name: Envinfo
run: npx envinfo
- name: TSAN Build
run: |
mkdir build-tsan
(cd build-tsan && cmake .. -G Ninja -DBUILD_TESTING=ON -DTSAN=ON -DCMAKE_BUILD_TYPE=Release)
cmake --build build-tsan
- name: TSAN Test
continue-on-error: true # currently permit failures
run: |
./build-tsan/uv_run_tests_a
- name: ASAN Build
run: |
mkdir build-asan
@ -39,3 +31,32 @@ jobs:
- name: ASAN Test
run: |
./build-asan/uv_run_tests_a
- name: MSAN Build
run: |
mkdir build-msan
(cd build-msan && cmake .. -G Ninja -DBUILD_TESTING=ON -DMSAN=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang)
cmake --build build-msan
- name: MSAN Test
run: |
./build-msan/uv_run_tests_a
- name: TSAN Build
run: |
mkdir build-tsan
(cd build-tsan && cmake .. -G Ninja -DBUILD_TESTING=ON -DTSAN=ON -DCMAKE_BUILD_TYPE=Release)
cmake --build build-tsan
- name: TSAN Test
# Note: path must be absolute because some tests chdir.
# TSan exits with an error when it can't find the file.
run: |
env TSAN_OPTIONS="suppressions=$PWD/tsansupp.txt" ./build-tsan/uv_run_tests_a
- name: UBSAN Build
run: |
mkdir build-ubsan
(cd build-ubsan && cmake .. -G Ninja -DBUILD_TESTING=ON -DUBSAN=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang)
cmake --build build-ubsan
- name: UBSAN Test
run: |
./build-ubsan/uv_run_tests_a

3
deps/libuv/.mailmap vendored
View File

@ -29,6 +29,7 @@ Keno Fischer <kenof@stanford.edu> <kfischer+github@college.harvard.edu>
Keno Fischer <kenof@stanford.edu> <kfischer@college.harvard.edu>
Leith Bade <leith@leithalweapon.geek.nz> <leith@mapbox.com>
Leonard Hecker <leonard.hecker91@gmail.com> <leonard@hecker.io>
Lewis Russell <me@lewisr.dev> <lewis6991@gmail.com>
Maciej Małecki <maciej.malecki@notimplemented.org> <me@mmalecki.com>
Marc Schlaich <marc.schlaich@googlemail.com> <marc.schlaich@gmail.com>
Michael <michael_dawson@ca.ibm.com>
@ -60,5 +61,7 @@ gengjiawen <technicalcute@gmail.com>
jBarz <jBarz@users.noreply.github.com> <jbarboza@ca.ibm.com>
jBarz <jBarz@users.noreply.github.com> <jbarz@users.noreply.github.com>
ptlomholt <pt@lomholt.com>
theanarkh <2923878201@qq.com> <theratliter@gmail.com>
tjarlama <59913901+tjarlama@users.noreply.github.com> <tjarlama@gmail.com>
ywave620 <rogertyang@tencent.com> <60539365+ywave620@users.noreply.github.com>
zlargon <zlargon1988@gmail.com>

View File

@ -5,7 +5,11 @@ sphinx:
configuration: null
fail_on_warning: false
build:
os: "ubuntu-22.04"
tools:
python: "3.9"
python:
version: 3.8
install:
- requirements: docs/requirements.txt

31
deps/libuv/AUTHORS vendored
View File

@ -517,3 +517,34 @@ chucksilvers <chuq@chuq.com>
Sergey Fedorov <vital.had@gmail.com>
theanarkh <2923878201@qq.com>
Samuel Cabrero <samuelcabrero@gmail.com>
自发对称破缺 <429839446@qq.com>
Luan Devecchi <luan@engineer.com>
Steven Schveighoffer <schveiguy@gmail.com>
number201724 <number201724@me.com>
Daniel <reymond315qq@gmail.com>
Christian Clason <christian.clason@uni-due.de>
ywave620 <rogertyang@tencent.com>
jensbjorgensen <jbj1@ultraemail.net>
daomingq <daoming.qiu@intel.com>
Qix <Qix-@users.noreply.github.com>
Edward Humes <29870961+aurxenon@users.noreply.github.com>
Tim Besard <tim.besard@gmail.com>
Sergey Rubanov <chi187@gmail.com>
Stefan Stojanovic <StefanStojanovic@users.noreply.github.com>
Zvicii <zvicii@qq.com>
dundargoc <33953936+dundargoc@users.noreply.github.com>
Jack·Boos·Yu <47264268+JackBoosY@users.noreply.github.com>
panran <310762957@qq.com>
Tamás Bálint Misius <lbphacker@gmail.com>
Bruno Passeri <Varstahl@users.noreply.github.com>
Jason Zhang <xzha4350@gmail.com>
Lewis Russell <me@lewisr.dev>
sivadeilra <arlie.davis@gmail.com>
cui fliter <imcusg@gmail.com>
Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
Stefan Karpinski <stefan@karpinski.org>
liuxiang88 <94350585+liuxiang88@users.noreply.github.com>
Jeffrey H. Johnson <trnsz@pobox.com>
Abdirahim Musse <33973272+abmusse@users.noreply.github.com>
小明 <7737673+caobug@users.noreply.github.com>

View File

@ -1,8 +1,13 @@
cmake_minimum_required(VERSION 3.4)
project(libuv LANGUAGES C)
cmake_policy(SET CMP0057 NEW) # Enable IN_LIST operator
cmake_policy(SET CMP0064 NEW) # Support if (TEST) operator
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW) # Enable MSVC_RUNTIME_LIBRARY setting
endif()
if(POLICY CMP0092)
cmake_policy(SET CMP0092 NEW) # disable /W3 warning, if possible
endif()
project(libuv LANGUAGES C)
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@ -17,9 +22,13 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_C_STANDARD 90)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
option(LIBUV_BUILD_SHARED "Build shared lib" ON)
cmake_dependent_option(LIBUV_BUILD_TESTS
"Build the unit tests when BUILD_TESTING is enabled and we are the root project" ON
"BUILD_TESTING;CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF)
"BUILD_TESTING;LIBUV_BUILD_SHARED;CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF)
cmake_dependent_option(LIBUV_BUILD_BENCH
"Build the benchmarks when building unit tests and we are the root project" ON
"LIBUV_BUILD_TESTS" OFF)
@ -27,28 +36,61 @@ cmake_dependent_option(LIBUV_BUILD_BENCH
# Qemu Build
option(QEMU "build for qemu" OFF)
if(QEMU)
add_definitions(-D__QEMU__=1)
list(APPEND uv_defines __QEMU__=1)
endif()
# Note: these are mutually exclusive.
option(ASAN "Enable AddressSanitizer (ASan)" OFF)
option(MSAN "Enable MemorySanitizer (MSan)" OFF)
option(TSAN "Enable ThreadSanitizer (TSan)" OFF)
option(UBSAN "Enable UndefinedBehaviorSanitizer (UBSan)" OFF)
if((ASAN OR TSAN) AND NOT (CMAKE_C_COMPILER_ID MATCHES "AppleClang|GNU|Clang"))
message(SEND_ERROR "Sanitizer support requires clang or gcc. Try again with -DCMAKE_C_COMPILER.")
if(MSAN AND NOT CMAKE_C_COMPILER_ID MATCHES "AppleClang|Clang")
message(SEND_ERROR "MemorySanitizer requires clang. Try again with -DCMAKE_C_COMPILER=clang")
endif()
if(ASAN)
add_definitions(-D__ASAN__=1)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
list(APPEND uv_defines __ASAN__=1)
if(CMAKE_C_COMPILER_ID MATCHES "AppleClang|GNU|Clang")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
elseif(MSVC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address")
else()
message(SEND_ERROR "AddressSanitizer support requires clang, gcc, or msvc. Try again with -DCMAKE_C_COMPILER.")
endif()
endif()
if(MSAN)
list(APPEND uv_defines __MSAN__=1)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=memory")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=memory")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=memory")
endif()
if(TSAN)
add_definitions(-D__TSAN__=1)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
list(APPEND uv_defines __TSAN__=1)
if(CMAKE_C_COMPILER_ID MATCHES "AppleClang|GNU|Clang")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=thread")
else()
message(SEND_ERROR "ThreadSanitizer support requires clang or gcc. Try again with -DCMAKE_C_COMPILER.")
endif()
endif()
if(UBSAN)
list(APPEND uv_defines __UBSAN__=1)
if(CMAKE_C_COMPILER_ID MATCHES "AppleClang|GNU|Clang")
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fno-omit-frame-pointer -fsanitize=undefined")
set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=undefined")
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fno-omit-frame-pointer -fsanitize=undefined")
elseif(MSVC)
set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=undefined")
else()
message(SEND_ERROR "UndefinedBehaviorSanitizer support requires clang, gcc, or msvc. Try again with -DCMAKE_C_COMPILER.")
endif()
endif()
# Compiler check
@ -126,6 +168,7 @@ set(uv_sources
src/random.c
src/strscpy.c
src/strtok.c
src/thread-common.c
src/threadpool.c
src/timer.c
src/uv-common.c
@ -140,7 +183,10 @@ if(WIN32)
advapi32
iphlpapi
userenv
ws2_32)
ws2_32
dbghelp
ole32
uuid)
list(APPEND uv_sources
src/win/async.c
src/win/core.c
@ -216,15 +262,11 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Android")
list(APPEND uv_defines _GNU_SOURCE)
list(APPEND uv_libraries dl)
list(APPEND uv_sources
src/unix/linux-core.c
src/unix/linux-inotify.c
src/unix/linux-syscalls.c
src/unix/linux.c
src/unix/procfs-exepath.c
src/unix/pthread-fixes.c
src/unix/random-getentropy.c
src/unix/random-getrandom.c
src/unix/random-sysctl-linux.c
src/unix/epoll.c)
src/unix/random-sysctl-linux.c)
endif()
if(APPLE OR CMAKE_SYSTEM_NAME MATCHES "Android|Linux")
@ -270,22 +312,14 @@ if(CMAKE_SYSTEM_NAME STREQUAL "GNU")
src/unix/hurd.c)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "kFreeBSD")
list(APPEND uv_defines _GNU_SOURCE)
list(APPEND uv_libraries dl freebsd-glue)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
list(APPEND uv_defines _GNU_SOURCE _POSIX_C_SOURCE=200112)
list(APPEND uv_libraries dl rt)
list(APPEND uv_sources
src/unix/linux-core.c
src/unix/linux-inotify.c
src/unix/linux-syscalls.c
src/unix/linux.c
src/unix/procfs-exepath.c
src/unix/random-getrandom.c
src/unix/random-sysctl-linux.c
src/unix/epoll.c)
src/unix/random-sysctl-linux.c)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "NetBSD")
@ -316,7 +350,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "OS390")
list(APPEND uv_defines _XOPEN_SOURCE=600)
list(APPEND uv_defines _XOPEN_SOURCE_EXTENDED)
list(APPEND uv_sources
src/unix/pthread-fixes.c
src/unix/os390.c
src/unix/os390-syscalls.c
src/unix/os390-proctitle.c)
@ -354,6 +387,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "OS400")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "SunOS")
if(CMAKE_SYSTEM_VERSION STREQUAL "5.10")
list(APPEND uv_defines SUNOS_NO_IFADDRS)
list(APPEND uv_libraries rt)
endif()
list(APPEND uv_defines __EXTENSIONS__ _XOPEN_SOURCE=500 _REENTRANT)
list(APPEND uv_libraries kstat nsl sendfile socket)
list(APPEND uv_sources
@ -388,25 +425,42 @@ if(APPLE OR CMAKE_SYSTEM_NAME MATCHES "DragonFly|FreeBSD|Linux|NetBSD|OpenBSD")
list(APPEND uv_test_libraries util)
endif()
add_library(uv SHARED ${uv_sources})
target_compile_definitions(uv
INTERFACE
USING_UV_SHARED=1
PRIVATE
BUILDING_UV_SHARED=1
${uv_defines})
target_compile_options(uv PRIVATE ${uv_cflags})
target_include_directories(uv
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>)
if(CMAKE_SYSTEM_NAME STREQUAL "OS390")
target_include_directories(uv PUBLIC $<BUILD_INTERFACE:${ZOSLIB_DIR}/include>)
set_target_properties(uv PROPERTIES LINKER_LANGUAGE CXX)
if(CYGWIN OR MSYS)
list(APPEND uv_defines _GNU_SOURCE)
list(APPEND uv_sources
src/unix/cygwin.c
src/unix/bsd-ifaddrs.c
src/unix/no-fsevents.c
src/unix/no-proctitle.c
src/unix/posix-hrtime.c
src/unix/posix-poll.c
src/unix/procfs-exepath.c
src/unix/sysinfo-loadavg.c
src/unix/sysinfo-memory.c)
endif()
if(LIBUV_BUILD_SHARED)
add_library(uv SHARED ${uv_sources})
target_compile_definitions(uv
INTERFACE
USING_UV_SHARED=1
PRIVATE
BUILDING_UV_SHARED=1
${uv_defines})
target_compile_options(uv PRIVATE ${uv_cflags})
target_include_directories(uv
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>)
if(CMAKE_SYSTEM_NAME STREQUAL "OS390")
target_include_directories(uv PUBLIC $<BUILD_INTERFACE:${ZOSLIB_DIR}/include>)
set_target_properties(uv PROPERTIES LINKER_LANGUAGE CXX)
endif()
target_link_libraries(uv ${uv_libraries})
set_target_properties(uv PROPERTIES OUTPUT_NAME "uv")
endif()
target_link_libraries(uv ${uv_libraries})
add_library(uv_a STATIC ${uv_sources})
target_compile_definitions(uv_a PRIVATE ${uv_defines})
@ -422,6 +476,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "OS390")
set_target_properties(uv_a PROPERTIES LINKER_LANGUAGE CXX)
endif()
target_link_libraries(uv_a ${uv_libraries})
set_target_properties(uv_a PROPERTIES OUTPUT_NAME "uv")
if(MSVC)
set_target_properties(uv_a PROPERTIES PREFIX "lib")
endif()
if(LIBUV_BUILD_TESTS)
# Small hack: use ${uv_test_sources} now to get the runner skeleton,
@ -584,6 +642,7 @@ if(LIBUV_BUILD_TESTS)
test/test-tcp-rst.c
test/test-tcp-shutdown-after-write.c
test/test-tcp-try-write.c
test/test-tcp-write-in-a-row.c
test/test-tcp-try-write-error.c
test/test-tcp-unexpected-read.c
test/test-tcp-write-after-connect.c
@ -592,6 +651,7 @@ if(LIBUV_BUILD_TESTS)
test/test-tcp-write-to-half-open-connection.c
test/test-tcp-writealot.c
test/test-test-macros.c
test/test-thread-affinity.c
test/test-thread-equal.c
test/test-thread.c
test/test-threadpool-cancel.c
@ -624,6 +684,7 @@ if(LIBUV_BUILD_TESTS)
test/test-udp-sendmmsg-error.c
test/test-udp-send-unreachable.c
test/test-udp-try-send.c
test/test-udp-recv-in-a-row.c
test/test-uname.c
test/test-walk-handles.c
test/test-watcher-cross-stop.c)
@ -667,27 +728,36 @@ string(REPLACE ";" " " LIBS "${LIBS}")
file(STRINGS configure.ac configure_ac REGEX ^AC_INIT)
string(REGEX MATCH "([0-9]+)[.][0-9]+[.][0-9]+" PACKAGE_VERSION "${configure_ac}")
set(UV_VERSION_MAJOR "${CMAKE_MATCH_1}")
# The version in the filename is mirroring the behaviour of autotools.
set_target_properties(uv PROPERTIES
VERSION ${UV_VERSION_MAJOR}.0.0
SOVERSION ${UV_VERSION_MAJOR})
set(includedir ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR})
set(libdir ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR})
set(prefix ${CMAKE_INSTALL_PREFIX})
configure_file(libuv.pc.in libuv.pc @ONLY)
configure_file(libuv-static.pc.in libuv-static.pc @ONLY)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES ${PROJECT_BINARY_DIR}/libuv.pc ${PROJECT_BINARY_DIR}/libuv-static.pc
install(FILES LICENSE-extra DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES ${PROJECT_BINARY_DIR}/libuv-static.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(TARGETS uv EXPORT libuvConfig
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(TARGETS uv_a EXPORT libuvConfig
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(EXPORT libuvConfig DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libuv)
install(EXPORT libuvConfig
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/libuv
NAMESPACE libuv::)
if(LIBUV_BUILD_SHARED)
# The version in the filename is mirroring the behaviour of autotools.
set_target_properties(uv PROPERTIES
VERSION ${UV_VERSION_MAJOR}.0.0
SOVERSION ${UV_VERSION_MAJOR})
configure_file(libuv.pc.in libuv.pc @ONLY)
install(FILES ${PROJECT_BINARY_DIR}/libuv.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(TARGETS uv EXPORT libuvConfig
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
if(MSVC)
set(CMAKE_DEBUG_POSTFIX d)

354
deps/libuv/ChangeLog vendored
View File

@ -1,4 +1,356 @@
2022.07.12, Version 1.44.2 (Stable)
2023.06.30, Version 1.46.0 (Stable)
Changes since version 1.45.0:
* Add SHA to ChangeLog (Santiago Gimeno)
* misc: update readthedocs config (Jameson Nash)
* test: remove erroneous RETURN_SKIP (Ben Noordhuis)
* android: disable io_uring support (Ben Noordhuis)
* linux: add some more iouring backed fs ops (Santiago Gimeno)
* build: add autoconf option for disable-maintainer-mode (Jameson Nash)
* fs: use WTF-8 on Windows (Stefan Karpinski)
* unix,win: replace QUEUE with struct uv__queue (Ben Noordhuis)
* linux: fs_read to use io_uring if iovcnt > IOV_MAX (Santiago Gimeno)
* ios: fix uv_getrusage() ru_maxrss calculation (Ben Noordhuis)
* include: update outdated code comment (Ben Noordhuis)
* linux: support abstract unix sockets (Ben Noordhuis)
* unix,win: add UV_PIPE_NO_TRUNCATE flag (Ben Noordhuis)
* unix: add loongarch support (liuxiang88)
* doc: add DPS8M to LINKS.md (Jeffrey H. Johnson)
* include: add EUNATCH errno mapping (Abdirahim Musse)
* src: don't run timers if loop is stopped/unref'd (Trevor Norris)
* win: fix -Wpointer-to-int-cast warning (Ben Noordhuis)
* test,win: fix -Wunused-variable warning (Ben Noordhuis)
* test,win: fix -Wformat warning (Ben Noordhuis)
* linux: work around io_uring IORING_OP_CLOSE bug (Ben Noordhuis)
* win: remove unused functions (Ben Noordhuis)
* bench: add bench to check uv_loop_alive (Trevor Norris)
* test: add uv_cancel test for threadpool (Trevor Norris)
* unix: skip prohibited syscalls on tvOS and watchOS (小明)
* unix,fs: make no_pwritev access thread-safe (Santiago Gimeno)
* unix: fix build for lower versions of Android (小明)
2023.05.19, Version 1.45.0 (Stable), 96e05543f53b19d9642b4b0dd73b86ad3cea313e
Changes since version 1.44.2:
* win: remove stdint-msvc2008.h (Ben Noordhuis)
* android: remove pthread-fixes.c (Ben Noordhuis)
* build: enable MSVC_RUNTIME_LIBRARY setting (自发对称破缺)
* unix: switch to c11 atomics (Ben Noordhuis)
* unix: don't accept() connections in a loop (Ben Noordhuis)
* win: fix off-by-1 buffer overrun in uv_exepath() (Ben Noordhuis)
* build: switch ci from macos-10.15 to macos-11 (Ben Noordhuis)
* win: fix thread race in uv_cwd() and uv_chdir() (Ben Noordhuis)
* unix,win: remove UV_HANDLE_SHUTTING flag (Santiago Gimeno)
* win: support Windows 11 in uv_os_uname() (Luan Devecchi)
* unix: fix uv_getrusage() ru_maxrss reporting (Ben Noordhuis)
* doc: add note about offset -1 in uv_fs_read/write (Steven Schveighoffer)
* test: fix musl libc.a dlerror() test expectation (Ben Noordhuis)
* kqueue: DRY file descriptor deletion logic (Ben Noordhuis)
* linux: teach uv_get_constrained_memory() cgroupsv2 (Ben Noordhuis)
* build: upgrade qemu-user-static package (Ben Noordhuis)
* linux: move epoll.c back into linux-core.c (Ben Noordhuis)
* unix: remove pre-macos 10.8 compatibility hack (Ben Noordhuis)
* unix,win: fix memory leak in uv_fs_scandir() (Ben Noordhuis)
* build: restore qemu download logic (Ben Noordhuis)
* win: fix uv__pipe_accept memory leak (number201724)
* doc: update LINKS.md (Daniel)
* unix: simplify atomic op in uv_tty_reset_mode() (Ben Noordhuis)
* build: add LIBUV_BUILD_SHARED cmake option (Christian Clason)
* linux: remove unused or obsolete syscall wrappers (Ben Noordhuis)
* linux: merge files back into single file (Ben Noordhuis)
* stream: process more than one write req per loop tick (ywave620)
* unix,win: give thread pool threads an 8 MB stack (Ben Noordhuis)
* build: add MemorySanitizer (MSAN) support (Ben Noordhuis)
* doc: add uv_poll_cb status==UV_EBADF note (jensbjorgensen)
* build: support AddressSanitizer on MSVC (Jameson Nash)
* win,pipe: improve method of obtaining pid for ipc (number201724)
* thread: add support for affinity (daomingq)
* include: map ENODATA error code (Ben Noordhuis)
* build: remove bashism from autogen.sh (Santiago Gimeno)
* win,tcp,udp: remove "active streams" optimization (Saúl Ibarra Corretgé)
* win: drop code checking for Windows XP / Server 2k3 (Saúl Ibarra Corretgé)
* unix,win: fix 'sprintf' is deprecated warning (twosee)
* doc: mention close_cb can be NULL (Qix)
* win: optimize udp receive performance (ywave620)
* win: fix an incompatible types warning (twosee)
* doc: document 0 return value for free/total memory (Ben Noordhuis)
* darwin: use hw.cpufrequency again for frequency info (Jameson Nash)
* win,test: change format of TEST_PIPENAME's (Santiago Gimeno)
* win,pipe: fixes in uv_pipe_connect() (Santiago Gimeno)
* misc: fix return value of memory functions (theanarkh)
* src: add new metrics APIs (Trevor Norris)
* thread: add uv_thread_getcpu() (daomingq)
* build: don't use ifaddrs.h on solaris 10 (Edward Humes)
* unix,win: add uv_get_available_memory() (Tim Besard)
* test: fix -Wunused-but-set-variable warnings (Ben Noordhuis)
* doc: bump min supported linux and freebsd versions (Ben Noordhuis)
* Add Socket Runtime to the LINKS.md (Sergey Rubanov)
* unix: drop kfreebsd support (Ben Noordhuis)
* win: fix fstat for pipes and character files (Stefan Stojanovic)
* win: fix -Wunused-variable warning (Ben Noordhuis)
* win: fix -Wunused-function warning (Ben Noordhuis)
* build: drop qemu-alpha from ci matrix (Ben Noordhuis)
* win: move child_stdio_buffer out of uv_process_t (Santiago Gimeno)
* test: fix some unreachable code warnings (Santiago Gimeno)
* linux: simplify uv_uptime() (Ben Noordhuis)
* test: unflake fs_event_watch_dir test (Ben Noordhuis)
* darwin: remove unused fsevents symbol lookups (Ben Noordhuis)
* build: add define guard around UV_EXTERN (Zvicii)
* build: add UndefinedBehaviorSanitizer support (Ben Noordhuis)
* build: enable platform_output test on qemu (Ben Noordhuis)
* linux: handle cpu hotplugging in uv_cpu_info() (Ben Noordhuis)
* build: remove unnecessary policy setting (dundargoc)
* docs: add vcpkg instruction step (Jack·Boos·Yu)
* win,fs: fix readlink errno for a non-symlink file (Darshan Sen)
* misc: extend getpw to take uid as an argument (Jameson Nash)
* unix,win: use static_assert when available (Ben Noordhuis)
* docs: delete code Makefile (Jameson Nash)
* docs: add CI for docs PRs (Jameson Nash)
* docs: update Sphinx version on RTD (Jameson Nash)
* doc: clean up license file (Ben Noordhuis)
* test: fix some warnings when compiling tests (panran)
* build,win: add mingw-w64 CI configuration (Jameson Nash)
* build: add CI for distcheck (Jameson Nash)
* unix: remove busy loop from uv_async_send (Jameson Nash)
* doc: document uv_fs_cb type (Tamás Bálint Misius)
* build: Improve build by cmake for Cygwin (erw7)
* build: add libuv:: namespace to libuvConfig.cmake (AJ Heller)
* test: fix ThreadSanitizer thread leak warning (Ben Noordhuis)
* test: fix ThreadSanitizer data race warning (Ben Noordhuis)
* test: fix ThreadSanitizer data race warning (Ben Noordhuis)
* test: fix ThreadSanitizer data race warning (Ben Noordhuis)
* test: cond-skip fork_threadpool_queue_work_simple (Ben Noordhuis)
* test: cond-skip signal_multiple_loops (Ben Noordhuis)
* test: cond-skip tcp_writealot (Ben Noordhuis)
* build: promote tsan ci to must-pass (Ben Noordhuis)
* build: add CI for OpenBSD and FreeBSD (James McCoy)
* build,test: fix distcheck errors (Jameson Nash)
* test: remove bad tty window size assumption (Ben Noordhuis)
* darwin,process: feed kevent the signal to reap children (Jameson Nash)
* unix: abort on clock_gettime() error (Ben Noordhuis)
* test: remove timing-sensitive check (Ben Noordhuis)
* unix: DRY and fix tcp bind error path (Jameson Nash)
* macos: fix fsevents thread race conditions (Ben Noordhuis)
* win: fix leak in uv_chdir (Trevor Norris)
* test: make valgrind happy (Trevor Norris)
* barrier: wait for prior out before next in (Jameson Nash)
* test: fix visual studio 2015 build error (Ben Noordhuis)
* linux: fix ceph copy error truncating readonly files (Bruno Passeri)
* test: silence more valgrind warnings (Trevor Norris)
* doc: add entries to LINKS.md (Trevor Norris)
* win,unix: change execution order of timers (Trevor Norris)
* doc: add trevnorris to maintainers (Trevor Norris)
* linux: remove epoll_pwait() emulation code path (Ben Noordhuis)
* linux: replace unsafe macro with inline function (Ben Noordhuis)
* linux: remove arm oabi support (Ben Noordhuis)
* unix,sunos: SO_REUSEPORT not valid on all sockets (Stacey Marshall)
* doc: consistent single backquote in misc.rst (Jason Zhang)
* src: switch to use C11 atomics where available (Trevor Norris)
* test: don't use static buffer for formatting (Ben Noordhuis)
* linux: introduce io_uring support (Ben Noordhuis)
* linux: fix academic valgrind warning (Ben Noordhuis)
* test: disable signal test under ASan and MSan (Ben Noordhuis)
* linux: add IORING_OP_OPENAT support (Ben Noordhuis)
* linux: add IORING_OP_CLOSE support (Ben Noordhuis)
* linux: remove bug workaround for obsolete kernels (Ben Noordhuis)
* doc: update active maintainers list (Ben Noordhuis)
* test: add ASSERT_OK (Trevor Norris)
* src: fix events/events_waiting metrics counter (Trevor Norris)
* unix,win: add uv_clock_gettime() (Ben Noordhuis)
* build: remove freebsd and openbsd buildbots (Ben Noordhuis)
* win: fix race condition in uv__init_console() (sivadeilra)
* linux: fix logic bug in sqe ring space check (Ben Noordhuis)
* linux: use io_uring to batch epoll_ctl calls (Ben Noordhuis)
* macos: update minimum supported version (Santiago Gimeno)
* docs: fix some typos (cui fliter)
* unix: use memcpy() instead of type punning (Ben Noordhuis)
* test: add additional assert (Mohammed Keyvanzadeh)
* build: export compile_commands.json (Lewis Russell)
* win,process: write minidumps when sending SIGQUIT (Elliot Saba)
* unix: constrained_memory should return UINT64_MAX (Tim Besard)
* unix: handle CQ overflow in iou ring (Santiago Gimeno)
* unix: remove clang compiler warning pragmas (Ben Noordhuis)
* win: fix mingw build (gengjiawen)
* test: fix -Wbool-compare compiler warning (Ben Noordhuis)
* win: define MiniDumpWithAvxXStateContext always (Santiago Gimeno)
* freebsd: hard-code UV_ENODATA definition (Santiago Gimeno)
* linux: work around EOWNERDEAD io_uring kernel bug (Ben Noordhuis)
* linux: fix WRITEV with lots of bufs using io_uring (Santiago Gimeno)
2022.07.12, Version 1.44.2 (Stable), 0c1fa696aa502eb749c2c4735005f41ba00a27b8
Changes since version 1.44.1:

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