142 Commits

Author SHA1 Message Date
ed4f1d6f2c android: Be smarter about the file watcher.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m12s
2025-08-13 17:51:04 -04:00
73f4a3407f ssb: Allow showing raw messages for contact messages. 2025-08-13 12:14:31 -04:00
6f11318e84 update: speedscope 1.23.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m56s
2025-08-13 12:06:47 -04:00
e88ee91f0e ssb: More reliably load private messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m50s
2025-08-06 12:10:51 -04:00
3f8daf257c update: OpenSSL 3.5.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-08-05 12:16:03 -04:00
dc387acadc ssb: Make progress bar brighter. 2025-08-02 12:16:07 -04:00
68aa41ab96 android: Tweaking random flags until ANRs subside.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m34s
2025-08-02 12:09:08 -04:00
85b23437b3 docs: Fix all the TODOCS. #39
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m17s
2025-08-02 09:07:45 -04:00
c59fba817d ssb: Show the progress indicator more consistently.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m54s
2025-07-31 12:48:45 -04:00
c3415ab75c docs: Expose the rest of core to docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m24s
2025-07-30 20:25:20 -04:00
f1d0151d71 ssb: Make the progress bar more indefinite-looking.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 20:04:34 -04:00
3c5c1756d1 ssb: A progress bar experiment. 2025-07-30 19:49:08 -04:00
6a6b65d1b3 build: Update nix config. Start building 0.2025.8, switching to calver.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 19:26:26 -04:00
81bd54dbe6 build: Let's build 0.0.33.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 29m24s
2025-07-30 18:21:17 -04:00
6a1bb0d3bc update: sqlite 3.50.4. 2025-07-30 17:47:06 -04:00
705e8b553f docs: Expose the rest of the core js to doxygen.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m35s
2025-07-27 21:48:18 -04:00
e4729b22f2 docs: Hook up doxygen to some of the core JS. Now maybe I am slightly incentivized to make progress on #39.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m18s
2025-07-27 15:04:17 -04:00
662112551a core: Remove/clean up some unhelpful logging. #124
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m9s
2025-07-27 14:06:46 -04:00
38fe88aab8 android: Guard aganst ANRs harder?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-07-27 13:17:49 -04:00
578c51faa0 update: npm.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m46s
2025-07-27 12:42:52 -04:00
a3ccc73b81 core: Move code.apps() to C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m44s
2025-07-18 11:53:31 -04:00
7312f4d43a httpd: Oops. 2025-07-18 11:53:31 -04:00
8b546c7e02 welcome: Add a gitea icon.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-07-18 10:10:16 -04:00
c0b6ff2e64 update: sqlite 3.50.3.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m12s
2025-07-17 20:57:10 -04:00
638b7cc1e5 ssb: Slight suggested follows cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-07-16 21:01:24 -04:00
05e54e1be0 httpd: More minor cleanup.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-16 20:44:42 -04:00
4c3299ead0 core: Begin to split some of the largest modules into smaller pieces, starting with HTTP endpoints.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m13s
2025-07-16 20:12:27 -04:00
1ef56b35ad update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m50s
2025-07-16 18:59:10 -04:00
061e79c295 welcome: Let's try this without server-side JS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m51s
2025-07-16 17:57:15 -04:00
5edfe732b1 core: Make it possible to host a web page with no additional server-side JS. An experiment in supporting simplicity and increased ability to be publicly searched and archived.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m55s
2025-07-16 12:47:16 -04:00
a8f9b67f71 cleanup: Remove the /ebt endpoint. The connections tab is superior.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m8s
2025-07-15 18:18:35 -04:00
de7fbf1eb7 update: picohttpparser.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m43s
2025-07-14 21:10:02 -04:00
a51a3d7e43 update: lit 3.3.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m49s
2025-07-11 21:36:28 -04:00
433b3b1003 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m18s
2025-07-09 18:59:03 -04:00
6703c5b584 ssb: Fix letterboxing/pillarboxing of images regardless of aspect ratio.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:53:27 -04:00
5f729efabe ssb: Load more messages at a time.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:35:51 -04:00
b2085b3f28 ssb: Try to keep the profile description from falling off the page. CSS, ugg. #126
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:25:47 -04:00
2f893494b0 ssb: Show the number of accounts followed on the profile show/hide followed button.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m51s
2025-07-09 17:50:06 -04:00
e26af21f63 ssb: Disambiguate some sqlite errors better. Today I learned there are various cases it doesn't update the error message, and prepare can succeed and not produce a statement.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m9s
2025-07-09 12:36:58 -04:00
7e1d738f8d ssb: Don't show the connection buttons in the main bar if there's a sidebar. 2025-07-09 12:09:03 -04:00
199448e11e build: Back to building 0.0.33-wip.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m35s
2025-07-07 12:40:32 -04:00
fdaabab807 android: Suppress a setDatabaseEnabled deprecation warning.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-07 12:12:37 -04:00
ca4560c5c9 update: speedscope 1.23.0. 2025-07-07 12:11:59 -04:00
2478f3064d android: Don't draw behind the status bar.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m5s
2025-07-06 07:39:09 -04:00
e9b8b43e7c ssb: Minor formatting around the connection sidebar buttons. 2025-07-06 07:27:01 -04:00
951155f1b6 build: Use this apksigner workaround for reproducible builds: https://gitlab.com/fdroid/fdroiddata/-/issues/3299.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 29m57s
2025-07-02 20:35:51 -04:00
1b678175ef build: Missed a number. Let's build a 0.0.32.1 with the version bumps and release it on F-Droid to see that we can.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-02 19:34:33 -04:00
8eb1f40eec build: Only build docs once, and use the right number of jobs. 2025-07-02 19:12:08 -04:00
235887b3bf android: Bump android versions in the gitea action, too.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 32m19s
2025-07-02 18:59:29 -04:00
0b3d66dd48 android: Google says: 'App must target Android 15 (API level 35) or higher'. Bump SDK versions while I'm in here.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-02 18:53:04 -04:00
beb9ef3754 ssb: Add a missing 'Encrypt' label. 2025-07-02 18:30:35 -04:00
9f6a480736 ssb: Nudge around some space around the compose widget. 2025-07-02 18:29:02 -04:00
b3bac2927d ssb: Shorten the timed query output.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-02 18:21:40 -04:00
ef389f2ba2 ssb: The unread line can be used to make everything above read. 2025-07-02 18:19:34 -04:00
ef21dc6ae8 ssb: Use img title=, not alt= for more browser support.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m42s
2025-07-02 12:49:14 -04:00
6e55b6b49e ssb: Show sync/stay connected options on the sidebar when relevant. 2025-07-02 12:41:03 -04:00
db115ef1bd cleanup: Just assessing how much we need OpenSSL and noticed cruft.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m55s
2025-07-01 18:42:43 -04:00
678838dbd5 update: OpenSSL 3.5.1.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-01 18:32:19 -04:00
586f87625d ssb: Revise the message queries slightly to not include votes where it might just be noisy.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 37m10s
2025-07-01 12:50:02 -04:00
1542370f9b ssb: Let's try not tracking vote unread status. It's useful that the channel is there, but it's too noisy for me to want to mark it as read.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-01 12:41:25 -04:00
1f7d5968c7 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m53s
2025-06-29 17:21:26 -04:00
39e51f7790 update: sqlite 3.50.2. 2025-06-29 17:20:50 -04:00
052663efbe ssb: Proof of concept to try to stay connected to a handful of peers. #130
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 37m27s
2025-06-28 13:47:58 -04:00
8f84ff2611 ssb: Collapse contact group users to only icons.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m44s
2025-06-27 12:46:15 -04:00
37e1c5d97b core: Use crypto_generichash. We don't need to roll our own FNV32a.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m37s
2025-06-25 21:10:05 -04:00
cef526bcf3 ssb: Fix the tab cycling order.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 37m34s
2025-06-25 20:23:01 -04:00
6af36cafa9 ssb: Wait, I don't think we need to close any connections here.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-25 20:10:22 -04:00
fca859d93d ssb: Are we inadvertantly closing connections when an inner tunnel request ends?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-25 20:00:44 -04:00
2178300d8d welcome: +PeachCloud.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m44s
2025-06-25 18:49:43 -04:00
636bdcce6b build: Start work on 0.0.33. nix => 0.0.32.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-25 18:34:59 -04:00
94b7703ca9 build: Let's build 0.0.32.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m51s
2025-06-25 12:58:25 -04:00
a391dd1316 update: prettier
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-25 12:57:29 -04:00
b6ba5211b7 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 37m36s
2025-06-23 21:34:27 -04:00
8e8e130045 docs: Ready 0.0.32 release notes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 37m36s
2025-06-23 12:52:31 -04:00
1f40bc1a0f core: Fix the one place where we called a JS function and didn't check for jobs to run as a result. Fixes getting stuck in the intro as a non-admin.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 38m21s
2025-06-22 18:53:20 -04:00
5437212222 build: Fix android.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 38m48s
2025-06-18 20:25:49 -04:00
a8ab845cd2 docs: Minor usage cleanup.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 20:13:16 -04:00
8cee6dc98b docs: Fix copy+paste lies.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 19:58:25 -04:00
70c2b73414 welcome: Introducing hermietildefriends.svg.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 19:43:24 -04:00
98013c4422 welcome: Direct to the latest release, obviously.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 19:16:34 -04:00
e9e22b762d docs: Make this format a little prettier-friendly.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 19:08:47 -04:00
620db19936 docs: Auto-generate a usage.md from the command-line interface.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 19:00:13 -04:00
94a79dd62c ssb: How did I not have this index? Makes channel unread queries significantly faster.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m43s
2025-06-18 18:26:11 -04:00
b56c3efde0 prettier
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m8s
2025-06-18 12:37:41 -04:00
066827f8f1 ssb: Deconstruct and instrument the channels unread query.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 12:36:59 -04:00
c3b65d9cd8 update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-18 12:12:57 -04:00
a15b916b06 ssb: Only print broadcast failures once.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m33s
2025-06-16 22:26:23 -04:00
31d0a5c233 intro: Don't meddle with settings as non-admin. #129
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m57s
2025-06-16 12:20:09 -04:00
140179e80a intro: Fix grammar. #128
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-16 12:12:50 -04:00
53cba2d7e4 Move all but the deleting off of the writer when deleting blobs and messages. Trying to eliminate a period of unresponsiveness near launch.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m41s
2025-06-15 08:46:07 -04:00
e54312d3b8 ssb: Only admins are offered the option to enable peer exchange.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m49s
2025-06-14 20:27:19 -04:00
cadc27b7b5 ssb: Filter out invalid RPC flags.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m12s
2025-06-12 12:50:38 -04:00
388b829ec1 ssb: Allow unread status for the mentions tab.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-12 12:25:58 -04:00
67861f0f33 ssb: Add some options to encourage getting connected to the connections sidebar section.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m46s
2025-06-11 20:53:51 -04:00
a1f1eb34d5 ssb: sequence: Sequential 32-bit integer starting at 1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m2s
2025-06-11 20:12:23 -04:00
2a6789063e ssb: Show/hide "Mark All Read" and the unread line more correctly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m53s
2025-06-11 19:22:21 -04:00
cbf1273a55 ssb: Squeeze some blood from the following_perf stone.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-11 19:12:59 -04:00
8143a23ced format 2025-06-11 18:52:56 -04:00
3c17810747 welcome: Update the welcome page to be a bit more direct and relevant. #124
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m17s
2025-06-11 18:39:15 -04:00
bea7a2e9ed core: Use JS_NewTypedArray.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m29s
2025-06-11 12:56:27 -04:00
2f0a2ac6b0 core: Prefer JS_AtomToCString() over JS_AtomToString() + JS_ToCString().
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-11 12:46:52 -04:00
18908b6b56 core: Ignore websocket errors when navigating away from the page (ie, Abnormal closure when switching apps). #60
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m21s
2025-06-11 12:10:28 -04:00
b135a210cc core: Avoid trivial snprintfs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m10s
2025-06-10 21:17:55 -04:00
3a2a829940 ssb: Disabling room support only disables the ability to tunnel through ourselves, not receive tunneled connections.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-10 20:36:59 -04:00
e56dd2dd2d core: Remove hitch tracking. Hasn't been an active problem, and the traces are sufficient to diagnose.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m36s
2025-06-10 12:44:56 -04:00
3f41a48bc7 ssb: Use message refs to get channel contents, not full-text search. Much faster for certain channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m43s
2025-06-09 12:16:09 -04:00
65ed53281a ssb: Shutdown hygiene. #108
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 36m20s
2025-06-07 18:00:05 -04:00
1121557a2e update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-07 17:29:45 -04:00
d4a7b86ee7 ssb: Revise private scanning slightly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m58s
2025-06-07 15:10:37 -04:00
626c18b04e ssb: Fix some correctly identified private messages not showing up in the private message feed. 2025-06-07 15:04:08 -04:00
bfa97ed7c7 log: Disable stdout buffering. I want to see output sooner in journalctl.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m18s
2025-06-07 13:24:55 -04:00
deae4d5367 ssb: Obviously close the readers before the writer. 2025-06-07 13:19:46 -04:00
899605c860 ssb: Actually prevent the sqlite WAL file from growing out of control by truncating when it grows to about 64MB of pages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m59s
2025-06-07 11:39:31 -04:00
dc9a279991 ssb: Free sqlite3_exec errors. 2025-06-07 10:36:44 -04:00
2a53892581 update: sqlite 3.50.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m58s
2025-06-07 08:04:34 -04:00
6bef0eb764 prettier
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m55s
2025-06-04 20:48:04 -04:00
462b40640c ssb: Don't show messages as unread when not in channels that allow unread status.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m29s
2025-06-04 19:56:02 -04:00
72e1b2025c core: Chasing shutdown issues some more. Logging. No more task promises once shutdown starts. #108
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-04 19:47:54 -04:00
fc7c4b1257 docs: Add a quick doc of how connecting Manyverse to tildefriends.net is supposed to work.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m3s
2025-06-04 18:45:18 -04:00
6c22c59056 room: Fix multiple issues with the core room app. Good gravy, it's like I don't want people to connect.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-04 18:24:31 -04:00
94c2b1184f prettier
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-04 18:01:39 -04:00
45231d703d ssb: Split recent votes into their own sidebar item as an experiment. #122 2025-06-04 18:00:46 -04:00
7882fcbe8f ssb: The red border is too much and stacks poorly. Let's try the same unread icon from the sidebar.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m51s
2025-06-04 12:24:04 -04:00
3bbc8c4d35 ssb: Consolidate the buttons around the compose UI. #122
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m53s
2025-06-02 21:47:35 -04:00
8ae10dc80b ssb: Add some more visibility that you're above the unread line. #122
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-02 21:24:03 -04:00
9b11c2c629 ssb: Expose a list of followed accounts on the profile page. #122
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-06-02 21:06:25 -04:00
e2a231fb4a ssb: Consolidated contact message groups a bit too much. 2025-06-02 12:14:06 -04:00
8a9502d1f2 ssb: More correct/thorough channel status.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m50s
2025-05-31 17:55:49 -04:00
534438df63 ssb: The message_refs table had loads of erroneous entries. This fixes that and adds channel/hashtag refs.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-05-31 17:42:17 -04:00
45a4feec96 ssb: Fix a use of undefined.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m36s
2025-05-31 16:52:37 -04:00
aa7a32395e ssb: Expose last successful connection time for stored connections.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-05-31 16:33:48 -04:00
ab9f57f044 ssb: Support expanding profile images the same as other images. #122
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-05-31 16:11:13 -04:00
4040d6aa08 ssb: Hide 'Mark as read' when we're not in a channel with a read status. #122
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m52s
2025-05-31 15:23:07 -04:00
1c96f5c35e ssb: Slight tweaks to 'This message is not currently available.' 2025-05-31 15:20:55 -04:00
4d3e42812d ssb: Condense follows/blocks more, and support replies to them. #122 2025-05-31 15:17:07 -04:00
f7b3711d4f ssb: Bury placeholder message blob ids in a context menu. #122
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 34m4s
2025-05-31 11:44:50 -04:00
2408e076ff ssb: Indicate connection status with some colors in the connections list in the sidebar. #122
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-05-31 11:28:09 -04:00
6f71ffb477 ssb: Treat most plaintext votes as 👍. #122
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 33m12s
2025-05-31 10:52:07 -04:00
214433f36a update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m41s
2025-05-29 12:39:31 -04:00
309b22732e update: sqlite 3.50.0.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-05-29 12:37:14 -04:00
6fe7687b2a docs: Update the screenshots.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m44s
2025-05-28 20:49:48 -04:00
a8cbf757ff build: Let's start work on 0.0.32. nix => 0.0.31.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m47s
2025-05-28 18:12:15 -04:00
111 changed files with 10235 additions and 5087 deletions

View File

@@ -48,7 +48,7 @@ jobs:
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make docs
make -j`nproc` docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
- name: Setup JDK
@@ -59,11 +59,11 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018'
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:

View File

@@ -342,7 +342,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
#
# Note see also the list of default file extension mappings.
EXTENSION_MAPPING =
EXTENSION_MAPPING = js=javascript
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable
@@ -944,6 +944,11 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched.
INPUT = README.md \
core/app.js \
core/client.js \
core/core.js \
core/http.js \
core/tfrpc.js \
docs/ \
src/
@@ -986,6 +991,7 @@ INPUT_FILE_ENCODING =
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.h \
*.js \
*.md
# The RECURSIVE tag can be used to specify whether or not subdirectories should
@@ -1051,7 +1057,7 @@ EXAMPLE_RECURSIVE = NO
# that contain images that are to be included in the documentation (see the
# \image command).
IMAGE_PATH =
IMAGE_PATH = docs/images/
# The INPUT_FILTER tag can be used to specify a program that doxygen should
# invoke to filter for each input file. Doxygen will invoke the filter program

View File

@@ -16,14 +16,14 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 37
VERSION_CODE_IOS := 13
VERSION_NUMBER := 0.0.31
VERSION_CODE := 41
VERSION_CODE_IOS := 16
VERSION_NUMBER := 0.2025.8-wip
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3490200.zip
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500400.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
@@ -106,10 +106,10 @@ LDFLAGS += \
-Wno-aggressive-loop-optimizations
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_TARGET_SDK_VERSION := 35
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/35.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/27.2.12479018
ANDROID_ARMV7A_TARGETS := \
out/androiddebug-armv7a/tildefriends \
@@ -650,6 +650,7 @@ SODIUM_SOURCES := \
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
deps/libsodium/src/libsodium/crypto_core/softaes/softaes.c \
deps/libsodium/src/libsodium/crypto_generichash/crypto_generichash.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
@@ -1054,7 +1055,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86_64/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym
@cd out/aab/staging; zip -u ../../../$@ BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym; cd ../../../
@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) $@
@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) --alignment-preserved $@
aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab
@@ -1116,12 +1117,12 @@ out/apk/TildeFriends-%.fdroid.unsigned.apk:
out/%.apk: out/apk/%.unsigned.apk
@echo "[apksigner] $(notdir $@)"
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $<
out/%.zopfli.apk: out/%.apk
@echo "[zopfli] $(notdir $@)"
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $@.zopfli
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
.PHONY: release-apk
@@ -1139,6 +1140,11 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo
x86releaseapkgo: out/TildeFriends-x86-release.apk ## Build, install, and run an x86 release Android APK.
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: x86releaseapkgo
apklog: ## Display Android log output.
@adb logcat *:S tildefriends
.PHONY: apklog
@@ -1483,6 +1489,18 @@ help: ## Display this help message.
.PHONY: help
.DEFAULT_GOAL := help
docs: debug
docs: ## Build HTML docs.
@echo '# CLI Usage\n' > docs/usage.md
@echo "## tildefriends -h" >> docs/usage.md
@echo '\n```' >> docs/usage.md
@out/debug/tildefriends -h >> docs/usage.md
@echo '```' >> docs/usage.md
@for command in $$(out/debug/tildefriends -h | grep -Po '[A-Za-z_]*(?= - )'); do
@ echo "\n## tildefriends $$command -h" >> docs/usage.md
@ echo '\n```' >> docs/usage.md
@ out/debug/tildefriends $$command -h >> docs/usage.md
@ echo '```' >> docs/usage.md
@done
@doxygen
.PHONY: docs

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💡",
"previous": "&ckE7T/dt9f1xV8jNSgXVcXYkAzMhU9689MRQbhOi9Wo=.sha256"
"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
}

View File

@@ -5,7 +5,10 @@ async function main() {
}
tfrpc.register(async function complete() {
if ((await core.globalSettingsGet('index')) == '/~core/intro/') {
if (
core.user?.credentials?.permissions?.administration &&
(await core.globalSettingsGet('index')) == '/~core/intro/'
) {
return await core.globalSettingsSet('index', '/~core/ssb/');
}
});

View File

@@ -82,7 +82,7 @@
<div class="w3-container w3-large w3-left-align">
<p>
Secure Scuttlebutt is a social network whose technical operation
attempts to mirrors human social interaction.
attempts to mirror human social interaction.
</p>
<ul>
<li>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -2,8 +2,8 @@ async function main() {
print(core.url);
let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
let port = await ssb.port();
let id = (await ssb.getServerIdentity()).substring(1);
let room = `net:${host}:${port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
let id = (await ssb.getServerIdentity()).substring(1).split('.')[0];
let room = `net:${host}:${port}~shs:${id}:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
await app.setDocument(`
<body style="color: #fff">
<h1>Server</h1>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&0Lxm4IgS3mpvSccP3bg7wNPACtLKMTbie51ea/vJbeg=.sha256"
"previous": "&TTGzyovmfKozjELCGPBFLLEXQcpfaArOMmqzemvz9J8=.sha256"
}

View File

@@ -106,6 +106,15 @@ tfrpc.register(async function sync() {
tfrpc.register(async function url() {
return core.url;
});
tfrpc.register(async function globalSettingsGet(key) {
return core.globalSettingsGet(key);
});
tfrpc.register(async function globalSettingsSet(key, value) {
return core.globalSettingsSet(key, value);
});
tfrpc.register(function isAdministrator() {
return core.user?.credentials?.permissions?.administration;
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -23,6 +23,9 @@ class TfElement extends LitElement {
url: {type: String},
private_messages: {type: Array},
recent_reactions: {type: Array},
is_administrator: {type: Boolean},
stay_connected: {type: Boolean},
progress: {type: Number},
};
}
@@ -54,6 +57,7 @@ class TfElement extends LitElement {
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) {
self.set_hash(hash);
self.reset_progress();
});
tfrpc.register(async function notifyNewMessage(id) {
await self.fetch_new_message(id);
@@ -73,6 +77,10 @@ class TfElement extends LitElement {
async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.is_administrator = await tfrpc.rpc.isAdministrator();
this.stay_connected =
this.is_administrator &&
(await tfrpc.rpc.globalSettingsGet('stay_connected'));
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
@@ -128,7 +136,13 @@ class TfElement extends LitElement {
}
next_channel(delta) {
let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
let channel_names = [
'',
'@',
'👍',
'🔐',
...this.channels.map((x) => '#' + x),
];
let index = channel_names.indexOf(this.hash.substring(1));
index = index != -1 ? index + delta : 0;
tfrpc.rpc.setHash(
@@ -302,11 +316,7 @@ class TfElement extends LitElement {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
ranges.push([
Math.max(i - k_chunk_size, 0),
Math.min(cache.range[0], i + k_chunk_size),
false,
]);
ranges.push([Math.max(i - k_chunk_size, 0), i, false]);
}
} else {
for (let i = 0; i < latest; i += k_chunk_size) {
@@ -322,7 +332,7 @@ class TfElement extends LitElement {
messages.rowid > ?1 AND
messages.rowid <= ?2 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
ORDER BY messages.rowid DESC
`,
[range[0], range[1]]
);
@@ -348,42 +358,83 @@ class TfElement extends LitElement {
return [cache.latest, cache.messages];
}
async query_timed(sql, args) {
let start = new Date();
let result = await tfrpc.rpc.query(sql, args);
let end = new Date();
console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim());
return result;
}
async load_channels_latest(following) {
let start_time = new Date();
let latest_private = this.get_latest_private(following);
let channels = await tfrpc.rpc.query(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel
UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`,
[
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
]
);
this.channels_latest = Object.fromEntries(
channels.map((x) => [x.channel, x.rowid])
);
const k_args = [
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
];
let channels = (
await Promise.all([
this.query_timed(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel
`,
k_args
),
this.query_timed(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN messages_refs ON messages.id = messages_refs.message
JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel
`,
k_args
),
this.query_timed(
`
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
`,
k_args
),
this.query_timed(
`
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`,
k_args
),
])
).flat();
let latest = {};
for (let row of channels) {
if (!latest[row.channel]) {
latest[row.channel] = row.rowid;
} else {
latest[row.channel] = Math.max(row.rowid, latest[row.channel]);
}
}
this.channels_latest = latest;
console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this;
start_time = new Date();
@@ -401,7 +452,28 @@ class TfElement extends LitElement {
this.schedule_load_latest();
}
reset_progress() {
if (this.progress === undefined) {
this._progress_start = new Date();
requestAnimationFrame(this.update_progress.bind(this));
}
}
update_progress() {
if (
!this.loading_latest &&
!this.loading_latest_scheduled &&
!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
) {
this.progress = undefined;
return;
}
this.progress = (new Date() - this._progress_start).valueOf();
requestAnimationFrame(this.update_progress.bind(this));
}
schedule_load_latest() {
this.reset_progress();
if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load();
@@ -446,6 +518,7 @@ class TfElement extends LitElement {
async load() {
this.loading_latest = true;
this.reset_progress();
try {
let start_time = new Date();
let whoami = this.whoami;
@@ -552,9 +625,14 @@ class TfElement extends LitElement {
.channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread}
@refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
.connections=${this.connections}
.private_messages=${this.private_messages}
.recent_reactions=${this.recent_reactions}
?is_administrator=${this.is_administrator}
?stay_connected=${this.stay_connected}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@@ -593,6 +671,7 @@ class TfElement extends LitElement {
async set_tab(tab) {
this.tab = tab;
if (tab === 'news') {
this.schedule_load_latest();
await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections');
@@ -605,6 +684,18 @@ class TfElement extends LitElement {
tfrpc.rpc.sync();
}
async toggle_stay_connected() {
let stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
let new_stay_connected = !this.stay_connected;
try {
if (new_stay_connected != stay_connected) {
await tfrpc.rpc.globalSettingsSet('stay_connected', new_stay_connected);
}
} finally {
this.stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
}
}
render() {
let self = this;
@@ -627,14 +718,26 @@ class TfElement extends LitElement {
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
>
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
</button>
${this.is_administrator && self.tab != 'news'
? html`
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
</button>
<button
class="w3-bar-item w3-button w3-ripple"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}
</button>
`
: undefined}
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
@@ -674,11 +777,23 @@ class TfElement extends LitElement {
Loading...
</div>`
: this.render_tab();
let progress =
this.progress !== undefined
? html`
<div style="position: absolute; width: 100%" id="progress">
<div
class="w3-theme-l3"
style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
></div>
</div>
`
: undefined;
return html`
<div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"
>
${progress}
<div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout">
${contents}

View File

@@ -446,12 +446,15 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
return html`<button
class="w3-button w3-bar-item w3-theme-d1"
@click=${attach_app}
>
Attach App
</button>`;
} else {
return html`<button
class="w3-button w3-theme-d1"
class="w3-button w3-bar-item w3-theme-d1"
@click=${() => (this.apps = null)}
>
Discard App
@@ -472,18 +475,9 @@ class TfComposeElement extends LitElement {
if (draft.content_warning !== undefined) {
return html`
<div class="w3-container w3-padding">
<p>
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<label for="cw">CW</label>
</p>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
</div>
`;
} else {
return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label>
`;
}
}
@@ -546,6 +540,31 @@ class TfComposeElement extends LitElement {
this.requestUpdate();
}
toggle_menu(event) {
event.srcElement.parentNode
.querySelector('.w3-dropdown-content')
.classList.toggle('w3-show');
}
connectedCallback() {
super.connectedCallback();
this._click_callback = this.document_click.bind(this);
document.body.addEventListener('mouseup', this._click_callback);
}
disconnectedCallback() {
super.disconnectedCallback();
document.body.removeEventListener('mouseup', this._click_callback);
}
document_click(event) {
let content = this.renderRoot.querySelector('.w3-dropdown-content');
let target = event.target;
if (content && !content.contains(target)) {
content.classList.remove('w3-show');
}
}
render() {
let self = this;
let draft = self.get_draft();
@@ -559,10 +578,10 @@ class TfComposeElement extends LitElement {
draft.encrypt_to !== undefined
? undefined
: html`<button
class="w3-button w3-theme-d1"
class="w3-button w3-bar-item w3-theme-d1"
@click=${() => this.set_encrypt([])}
>
🔐
🔐 Encrypt
</button>`;
let result = html`
<style>
@@ -583,7 +602,7 @@ class TfComposeElement extends LitElement {
: undefined}
${this.render_encrypt()}
</header>
<div class="w3-container w3-padding-small">
<div class="w3-container" style="padding: 0 0 16px 0">
<div class="w3-half">
<span
class="w3-input w3-theme-d1 w3-border"
@@ -604,7 +623,7 @@ class TfComposeElement extends LitElement {
${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x)
)}
<footer class="w3-container">
<footer>
${this.render_attach_app()} ${this.render_content_warning()}
${this.render_new_thread()}
<button
@@ -614,13 +633,43 @@ class TfComposeElement extends LitElement {
>
Submit
</button>
<button class="w3-button w3-theme-d1" @click=${this.attach}>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-theme-d1" @click=${this.discard}>
Discard
</button>
<div class="w3-dropdown-click">
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
⚙️
</button>
<div class="w3-dropdown-content w3-bar-block">
${this.get_draft().content_warning === undefined
? html`
<button
class="w3-button w3-bar-item w3-theme-d1"
@click=${() => self.set_content_warning('')}
>
Add Content Warning
</button>
`
: html`
<button
class="w3-button w3-bar-item w3-theme-d1"
@click=${() => self.set_content_warning(undefined)}
>
Remove Content Warning
</button>
`}
<button
class="w3-button w3-bar-item w3-theme-d1"
@click=${this.attach}
>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button
class="w3-button w3-bar-item w3-theme-d1"
@click=${this.discard}
>
Discard
</button>
</div>
</div>
</footer>
</div>
`;

View File

@@ -1,4 +1,11 @@
import {LitElement, html, repeat, render, unsafeHTML} from './lit-all.min.js';
import {
LitElement,
css,
html,
repeat,
render,
unsafeHTML,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js';
@@ -86,12 +93,18 @@ class TfMessageElement extends LitElement {
render_votes() {
function normalize_expression(expression) {
if (expression === 'Like' || expression === 'like' || !expression) {
return '👍';
} else if (expression === 'Unlike' || expression === 'unlike') {
if (
expression === 'Unlike' ||
expression === 'unlike' ||
expression == 'undig'
) {
return '👎';
} else if (expression === 'heart') {
return '❤️';
} else if (
(expression ?? '').split('').every((x) => x.charCodeAt(0) < 256)
) {
return '👍';
} else {
return expression;
}
@@ -178,12 +191,12 @@ class TfMessageElement extends LitElement {
div.style.display = 'grid';
let img = document.createElement('img');
img.src = link;
img.style.maxWidth = '100%';
img.style.maxHeight = '100%';
img.style.maxWidth = '100vw';
img.style.maxHeight = '100vh';
img.style.display = 'block';
img.style.margin = 'auto';
img.style.objectFit = 'contain';
img.style.width = '100%';
img.style.width = '100vw';
div.appendChild(img);
function image_close(event) {
document.body.removeChild(div);
@@ -297,31 +310,35 @@ class TfMessageElement extends LitElement {
return total;
}
expanded_key() {
return this.message?.id || this.messages?.map((x) => x.id).join(':');
}
set_expanded(expanded, tag) {
let key = this.expanded_key();
this.dispatchEvent(
new CustomEvent('tf-expand', {
bubbles: true,
composed: true,
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
detail: {id: key + (tag || ''), expanded: expanded},
})
);
}
toggle_expanded(tag) {
this.set_expanded(
!this.expanded[(this.message.id || '') + (tag || '')],
tag
);
let key = this.expanded_key();
this.set_expanded(!this.expanded[key + (tag || '')], tag);
}
is_expanded(tag) {
return this.expanded[(this.message.id || '') + (tag || '')];
let key = this.expanded_key();
return this.expanded[key + (tag || '')];
}
render_children() {
let self = this;
if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) {
if (!this.expanded[this.expanded_key()]) {
return html`
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
@@ -397,7 +414,7 @@ class TfMessageElement extends LitElement {
class_background() {
return this.message?.decrypted
? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
: this.allow_unread() && this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4';
}
@@ -489,7 +506,10 @@ class TfMessageElement extends LitElement {
return html`
<header class="w3-bar">
<span class="w3-bar-item">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${this.render_unread_icon()}<tf-user
id=${this.message.author}
.users=${this.users}
></tf-user>
</span>
${is_encrypted} ${this.render_menu()}
<div class="w3-bar-item w3-right" style="text-wrap: nowrap">
@@ -574,6 +594,56 @@ class TfMessageElement extends LitElement {
`;
}
content_group_by_author() {
let sorted = this.message.messages
.map((x) => [
x.author,
x.content.blocking !== undefined
? x.content.blocking
? 'is blocking'
: 'is no longer blocking'
: x.content.following !== undefined
? x.content.following
? 'is following'
: 'is no longer following'
: '',
x.content.contact,
x,
])
.sort();
let result = [];
let last;
let group;
for (let row of sorted) {
if (last && last[0] == row[0] && last[1] == row[1]) {
group.push(row[2]);
} else {
if (group) {
result.push({author: last[0], action: last[1], users: group});
}
last = row;
group = [row[2]];
}
}
if (group) {
result.push({author: last[0], action: last[1], users: group});
}
return result;
}
allow_unread() {
return (
this.channel == '@' ||
(!this.channel.startsWith('@') && !this.channel.startsWith('%'))
);
}
render_unread_icon() {
return this.allow_unread() && this.message?.rowid >= this.channel_unread
? html`✉️`
: undefined;
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
@@ -582,29 +652,94 @@ class TfMessageElement extends LitElement {
let class_background = this.class_background();
let self = this;
if (this.message?.type === 'contact_group') {
return this.render_frame(
html` ${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}`
);
if (this.expanded[this.expanded_key()]) {
return this.render_frame(html`
<div class="w3-padding">
${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(false)}
>
Collapse
</button>
`);
} else {
return this.render_frame(html`
<div class="w3-padding">
${this.content_group_by_author().map(
(x) => html`
<div>
<tf-user id=${x.author} .users=${this.users}></tf-user>
${x.action}
${x.users.map(
(y) => html`
<tf-user
id=${y}
.users=${this.users}
icon_only="true"
></tf-user>
`
)}
</div>
`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(true)}
>
Expand
</button>
`);
}
} else if (this.message.placeholder) {
return this.render_frame(
html`<div class="w3-padding">
<p>
<a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>${this.message.id}</a
html`<div>
<div class="w3-bar">
<a
class="w3-bar-item w3-panel w3-round-xlarge w3-theme-d1 w3-margin w3-button"
target="_top"
href=${'#' + encodeURIComponent(this.message?.id)}
>
(placeholder)
</p>
This message is not currently available.
</a>
<div class="w3-bar-item w3-right">
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
%
</button>
<div
class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
style="right: 48px"
>
<a
target="_top"
class="w3-button w3-bar-item"
href=${'#' + encodeURIComponent(this.message?.id)}
>View Message</a
>
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.copy_id}
>
Copy ID
</button>
</div>
</div>
</div>
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(
(x) => html`
@@ -631,7 +766,7 @@ class TfMessageElement extends LitElement {
}
if (content.image !== undefined) {
image = html`
<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
<div @click=${this.body_click}><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
`;
}
if (content.description !== undefined) {
@@ -654,25 +789,45 @@ class TfMessageElement extends LitElement {
</div>
`);
} else if (content.type == 'contact') {
return html`
<div class="w3-padding">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
is
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
</div>
`;
switch (this.format) {
case 'message':
default:
return this.render_frame(html`
<div class="w3-bar">
<div class="w3-bar-item">
<tf-user
id=${this.message.author}
.users=${this.users}
></tf-user>
is
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
</div>
${this.render_menu()} ${this.render_votes()}
${this.render_actions()}
</div>
`);
break;
case 'raw':
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
</div>
`);
break;
}
} else if (content.type == 'post') {
let self = this;
let body;

View File

@@ -14,6 +14,7 @@ class TfNewsElement extends LitElement {
channel: {type: String},
channel_unread: {type: Number},
recent_reactions: {type: Array},
hash: {type: String},
};
}
@@ -166,7 +167,10 @@ class TfNewsElement extends LitElement {
if (message?.content?.type === 'contact') {
group.push(message);
} else {
if (group.length > 0) {
if (group.length == 1) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
@@ -177,7 +181,10 @@ class TfNewsElement extends LitElement {
result.push(message);
}
}
if (group.length > 0) {
if (group.length == 1) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
@@ -187,15 +194,21 @@ class TfNewsElement extends LitElement {
return result;
}
unread_allowed() {
return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
}
load_and_render(messages) {
let messages_by_id = this.process_messages(messages);
let final_messages = this.group_following(
this.finalize_messages(messages_by_id)
);
let unread_rowid = -1;
for (let message of final_messages) {
if (message.rowid >= this.channel_unread) {
unread_rowid = message.rowid;
if (this.unread_allowed()) {
for (let message of final_messages) {
if (message.rowid >= this.channel_unread) {
unread_rowid = message.rowid;
}
}
}
return html`
@@ -220,7 +233,19 @@ class TfNewsElement extends LitElement {
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
<div style="color: #f00; padding: 8px">unread</div>
<button
style="color: #f00; padding: 8px"
class="w3-button"
@click=${() =>
this.dispatchEvent(
new Event('mark_all_read', {
bubbles: true,
composed: true,
})
)}
>
unread
</button>
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>

View File

@@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, until, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import {styles} from './tf-styles.js';
@@ -14,6 +14,7 @@ class TfProfileElement extends LitElement {
sequence: {type: Number},
following: {type: Boolean},
blocking: {type: Boolean},
show_followed: {type: Boolean},
};
}
@@ -166,6 +167,71 @@ class TfProfileElement extends LitElement {
navigator.clipboard.writeText(this.id);
}
show_image(link) {
let div = document.createElement('div');
div.style.left = 0;
div.style.top = 0;
div.style.width = '100%';
div.style.height = '100%';
div.style.position = 'fixed';
div.style.background = '#000';
div.style.zIndex = 100;
div.style.display = 'grid';
let img = document.createElement('img');
img.src = link;
img.style.maxWidth = '100vw';
img.style.maxHeight = '100vh';
img.style.display = 'block';
img.style.margin = 'auto';
img.style.objectFit = 'contain';
img.style.width = '100vw';
div.appendChild(img);
function image_close(event) {
document.body.removeChild(div);
window.removeEventListener('keydown', image_close);
}
div.onclick = image_close;
window.addEventListener('keydown', image_close);
document.body.appendChild(div);
}
body_click(event) {
if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src);
}
}
toggle_account_list(event) {
let content = event.srcElement.nextElementSibling;
this.show_followed = !this.show_followed;
}
async load_follows() {
let accounts = await tfrpc.rpc.following([this.id], 1);
return html`
<div class="w3-container">
<button
class="w3-button w3-block w3-theme-d1 followed_accounts"
@click=${this.toggle_account_list}
>
${this.show_followed ? 'Hide' : 'Show'} Followed Accounts
(${Object.keys(accounts).length})
</button>
<div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}>
<ul class="w3-ul w3-theme-d4 w3-border-theme">
${Object.keys(accounts).map(
(x) => html`
<li class="w3-border-theme">
<tf-user id=${x} .users=${this.users}></tf-user>
</li>
`
)}
</ul>
</div>
</div>
`;
}
render() {
this.load();
let self = this;
@@ -254,14 +320,14 @@ class TfProfileElement extends LitElement {
<header class="w3-container">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
</header>
<div class="w3-container">
<div class="w3-container" @click=${this.body_click}>
<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
@@ -280,6 +346,7 @@ class TfProfileElement extends LitElement {
Blocked by ${profile.blocked} identities.
</div>
</div>
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
<footer class="w3-container">
<p>
${edit}

View File

@@ -308,6 +308,12 @@ class TfTabConnectionsElement extends LitElement {
<div class="w3-bar-item">
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div>
<div>
<small
>Last connection:
${new Date(x.last_success * 1000)}</small
>
</div>
</div>
</div>
${this.render_message(x)}

View File

@@ -106,8 +106,15 @@ class TfTabNewsFeedElement extends LitElement {
}
async fetch_messages(start_time, end_time) {
this.dispatchEvent(
new CustomEvent('loadmessages', {
bubbles: true,
composed: true,
})
);
this.time_loading = [start_time, end_time];
let result;
const k_max_results = 64;
if (this.hash == '#@') {
result = await tfrpc.rpc.query(
`
@@ -118,7 +125,7 @@ class TfTabNewsFeedElement extends LitElement {
WHERE
messages.author != ?1 AND
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
ORDER BY timestamp DESC limit 20)
ORDER BY timestamp DESC limit ?5)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref
@@ -131,6 +138,7 @@ class TfTabNewsFeedElement extends LitElement {
JSON.stringify(this.following),
start_time,
end_time,
k_max_results,
]
);
} else if (this.hash.startsWith('#@')) {
@@ -140,7 +148,7 @@ class TfTabNewsFeedElement extends LitElement {
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence DESC LIMIT 20
ORDER BY sequence DESC LIMIT ?4
)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected
@@ -149,7 +157,7 @@ class TfTabNewsFeedElement extends LitElement {
UNION
SELECT TRUE AS is_primary, * FROM selected
`,
[this.hash.substring(1), start_time, end_time]
[this.hash.substring(1), start_time, end_time, k_max_results]
);
} else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query(
@@ -174,24 +182,24 @@ class TfTabNewsFeedElement extends LitElement {
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.content ->> 'channel' = ?4
WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote'
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
FROM messages_refs
JOIN messages ON messages.id = messages_refs.message
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote'
)
SELECT TRUE AS is_primary, all_news.* FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20
ORDER BY all_news.timestamp DESC LIMIT ?5
`,
[
JSON.stringify(this.following),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
k_max_results,
]
);
let t1 = new Date();
@@ -209,29 +217,46 @@ class TfTabNewsFeedElement extends LitElement {
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.sequence DESC LIMIT 20
ORDER BY messages.rowid DESC LIMIT ?4
`,
[JSON.stringify(this.private_messages), start_time, end_time]
[
JSON.stringify(this.private_messages),
start_time,
end_time,
k_max_results,
]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else if (this.hash == '#👍') {
result = await tfrpc.rpc.query(
`
WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'vote' AND
(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3
ORDER BY timestamp DESC limit ?4)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM votes
JOIN messages ON messages.id = votes.content ->> '$.vote.link'
UNION
SELECT TRUE AS is_primary, * FROM votes
`,
[JSON.stringify(this.following), start_time, end_time, k_max_results]
);
} else {
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
all_news AS (
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
),
news AS (
SELECT * FROM all_news
WHERE all_news.timestamp < ?3 AND (?2 IS NULL OR all_news.timestamp >= ?2)
ORDER BY timestamp DESC LIMIT 20
)
SELECT TRUE AS is_primary, news.* FROM news
SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
messages.content ->> 'type' != 'vote'
ORDER BY timestamp DESC LIMIT ?4
`,
[JSON.stringify(this.following), start_time, end_time]
[JSON.stringify(this.following), start_time, end_time, k_max_results]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
@@ -258,6 +283,13 @@ class TfTabNewsFeedElement extends LitElement {
];
}
unread_allowed() {
return (
this.hash == '#@' ||
(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
);
}
async load_more() {
this.loading++;
this.loading_canceled = false;
@@ -352,7 +384,8 @@ class TfTabNewsFeedElement extends LitElement {
this.messages = [];
this._messages_hash = this.hash;
}
this._messages_following = this.following;
this._messages_following = JSON.stringify(this.following);
this._private_messages = JSON.stringify(this.private_messages);
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
@@ -395,8 +428,8 @@ class TfTabNewsFeedElement extends LitElement {
if (
!this.messages ||
this._messages_hash !== this.hash ||
JSON.stringify(this._messages_following) !==
JSON.stringify(this.following)
this._messages_following !== JSON.stringify(this.following) ||
this._private_messages !== JSON.stringify(this.private_messages)
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
@@ -407,9 +440,16 @@ class TfTabNewsFeedElement extends LitElement {
if (!this.hash.startsWith('#%')) {
more = html`
<p>
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
${this.unread_allowed()
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.mark_all_read}
>
Mark All Read
</button>
`
: undefined}
<button
?disabled=${this.loading}
class="w3-button w3-theme-d1"
@@ -441,9 +481,14 @@ class TfTabNewsFeedElement extends LitElement {
`;
}
return cache(html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
${this.unread_allowed()
? html`<button
class="w3-button w3-theme-d1"
@click=${this.mark_all_read}
>
Mark All Read
</button>`
: undefined}
<tf-news
id="news"
whoami=${this.whoami}
@@ -452,9 +497,11 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
hash=${this.hash}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
.recent_reactions=${this.recent_reactions}
@mark_all_read=${this.mark_all_read}
></tf-news>
${more}
`);

View File

@@ -25,6 +25,9 @@ class TfTabNewsElement extends LitElement {
connections: {type: Array},
private_messages: {type: Array},
recent_reactions: {type: Array},
peer_exchange: {type: Boolean},
is_administrator: {type: Boolean},
stay_connected: {type: Boolean},
};
}
@@ -48,6 +51,7 @@ class TfTabNewsElement extends LitElement {
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
this.check_peer_exchange();
}
connectedCallback() {
@@ -60,6 +64,14 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
}
async check_peer_exchange() {
if (await tfrpc.rpc.isAdministrator()) {
this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
} else {
this.peer_exchange = undefined;
}
}
load_latest() {
let news = this.shadowRoot?.getElementById('news');
if (news) {
@@ -144,11 +156,8 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
}
compare_follows() {
const now = new Date().valueOf();
return function (a, b) {
return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
};
compare_follows(a, b) {
return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0;
}
suggested_follows() {
@@ -157,13 +166,24 @@ class TfTabNewsElement extends LitElement {
** pinned at the top.
*/
let self = this;
let now = new Date().valueOf();
return Object.entries(this.users)
.filter((x) => x[1].ts < now)
.filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows())
.sort(self.compare_follows)
.slice(0, 8)
.map((x) => x[0]);
}
async enable_peer_exchange() {
await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
await this.check_peer_exchange();
}
is_loading() {
return this.shadowRoot?.getElementById('news')?.loading;
}
render_sidebar() {
return html`
<div
@@ -177,6 +197,35 @@ class TfTabNewsElement extends LitElement {
>
&times;
</div>
${this.is_administrator
? html`
<button
class="w3-bar-item w3-button"
@click=${() =>
this.dispatchEvent(
new Event('refresh', {bubbles: true, composed: true})
)}
>
<span style="display: inline-block; width: 1.8em">↻</span>
Sync now
</button>
<button
class="w3-bar-item w3-button w3-ripple"
@click=${() =>
this.dispatchEvent(
new Event('toggle_stay_connected', {
bubbles: true,
composed: true,
})
)}
>
<span style="display: inline-block; width: 1.8em"
>${this.stay_connected ? '🔗' : '⛓️‍💥'}</span
>
${this.stay_connected ? 'Online mode' : 'Passive mode'}
</button>
`
: undefined}
${this.hash.startsWith('##') &&
this.channels.indexOf(this.hash.substring(2)) == -1
? html`
@@ -202,6 +251,12 @@ class TfTabNewsElement extends LitElement {
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>${this.unread_status('@')}@mentions</a
>
<a
href="#👍"
class="w3-bar-item w3-button"
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a
>
<a
href="#🔐"
class="w3-bar-item w3-button"
@@ -234,13 +289,42 @@ class TfTabNewsElement extends LitElement {
<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections">
<h4 style="margin: 0">Connections</h4>
</a>
${this.connections?.filter((x) => x.id)?.length == 0
? html`
<button
class=${'w3-bar-item w3-button' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
@click=${() =>
this.dispatchEvent(
new Event('refresh', {bubbles: true, composed: true})
)}
>
↻ Sync now
</button>
<button
class=${'w3-bar-item w3-button' +
(this.peer_exchange !== false ? ' w3-hide' : '')}
@click=${this.enable_peer_exchange}
>
Enable peer exchange
</button>
`
: undefined}
${this.connections
.filter((x) => x.id && !x.destroy_reason)
.filter((x) => x.id)
.map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
style=${x.destroy_reason
? 'border-left: 4px solid red; border-right: 4px solid red'
: x.connected
? x.flags?.one_shot
? 'border-left: 4px solid blue; border-right: 4px solid blue'
: 'border-left: 4px solid green; border-right: 4px solid green'
: ''}
id=${x.id}
fallback_name=${x.host}
.users=${this.users}

View File

@@ -7,6 +7,7 @@ class TfUserElement extends LitElement {
return {
id: {type: String},
fallback_name: {type: String},
icon_only: {type: Boolean},
users: {type: Object},
};
}
@@ -17,6 +18,7 @@ class TfUserElement extends LitElement {
super();
this.id = null;
this.fallback_name = null;
this.icon_only = false;
this.users = {};
}
@@ -32,9 +34,10 @@ class TfUserElement extends LitElement {
>😎</span
>`;
let name = this.users?.[this.id]?.name;
name = html`<a target="_top" href=${'#' + this.id}
>${name ?? this.fallback_name ?? this.id}</a
>`;
let name_string = name ?? this.fallback_name ?? this.id;
name = this.icon_only
? undefined
: html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`;
if (user) {
let image_link = user.image;
@@ -48,6 +51,7 @@ class TfUserElement extends LitElement {
class=${'w3-theme-l4 ' + shape}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
src="/${image_link}/view"
title=${name_string + ' (' + this.id + ')'}
/>`;
}
}

View File

@@ -50,9 +50,9 @@ function image(node, entering) {
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="');
this.lit('<img src="" title="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
this.lit('<img src="' + this.esc(node.destination) + '" title="');
}
}
this.disableTags += 1;

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&1o8MrBHfH42NnO+ruajwCmW/DUCb+IT1qtnAZI/agyo=.sha256"
"previous": "&5NkMRSgcMqCYF3xcLOBmaytkoxfV9zx4br7JladKPTs=.sha256"
}

View File

@@ -1,5 +0,0 @@
async function main() {
await app.setDocument(utf8Decode(getFile('index.html')));
}
main();

1
apps/welcome/gitea.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -28,23 +28,12 @@
<b>😎 Tilde Friends</b>
</h1>
<h1 class="w3-xxlarge w3-text-green">
<b
>the Secure Scuttlebutt decentralized social network client that's
<i>fancy🎩</i></b
>
<b>a Secure Scuttlebutt decentralized social network client</b>
</h1>
<p>
In addition to participating in Secure Scuttlebutt, Tilde Friends is
a platform for building, running, and sharing applications.
</p>
<p>
Available for lots of devices:
<i class="fa-brands fa-linux w3-xlarge"></i>
<i class="fa-brands fa-android w3-xlarge"></i>
<i class="fa-brands fa-apple w3-xlarge"></i>
<i class="fa fa-mobile-screen w3-xlarge"></i>
<i class="fa-brands fa-windows w3-xlarge"></i>
</p>
<a
class="w3-button w3-blue w3-padding-large"
href="https://www.tildefriends.net/~core/ssb/"
@@ -52,14 +41,16 @@
>
<a
class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/cory/tildefriends/releases"
href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
><i class="fa fa-download"></i> Download</a
>
<a
class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/cory/tildefriends"
><i class="fa fa-mug-hot"></i> Development</a
>
<img src="gitea.svg" style="height: 1em; margin: 0" />
Development
</a>
<a
class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/"
@@ -70,35 +61,6 @@
href="https://www.tildefriends.net/~cory/tildeblog/"
><i class="fa fa-solid fa-square-rss"></i> Blog</a
>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
on F-Droid</a
>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
>
<img src="googleplay.svg" style="height: 2em; margin: 0" />
Get it on Google Play (Open Testing)
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://testflight.apple.com/join/tXxgtSpE"
>
<img src="ios.svg" style="height: 2em; margin: 0" />
Get it on iOS (TestFlight)
</a>
</p>
</div>
<div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
@@ -116,15 +78,119 @@
<h2>First-time user checklist:</h2>
<ol type="1" style="text-align: left">
<li>
<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
<a
href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
>Download</a
>
Tilde Friends or use
<a href="https://www.tildefriends.net/"
>https://www.tildefriends.net/</a
>.
<div class="w3-cell-row">
<div class="w3-container w3-cell w3-mobile">
<h3>Mobile</h3>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
><img src="f-droid.svg" style="height: 2em; margin: 0" />
Get it on F-Droid</a
>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
>
<img
src="googleplay.svg"
style="height: 2em; margin: 0"
/>
Get it on Google Play (Open Testing)
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://testflight.apple.com/join/tXxgtSpE"
>
<img src="ios.svg" style="height: 2em; margin: 0" />
Get it on iOS (TestFlight)
</a>
</p>
<p>Just launch the app.</p>
</div>
<div class="w3-container w3-cell w3-mobile">
<h3>Web</h3>
<p>
<a
class="w3-button w3-round-large w3-blue w3-padding-large"
href="https://www.tildefriends.net/~core/ssb/"
>🦀 Try It</a
>
</p>
<p>
<a href="/login?return=/~core/intro"
>Register an account with tildefriends.net</a
>
to take it for a spin right away.
</p>
<h3>PeachCloud</h3>
<p>
Tilde Friends is also a part of 🍑☁️<a
href="https://peach-docs.commoninternet.net/"
>PeachCloud</a
>, which is available on
<a href="https://apps.yunohost.org/app/peachpub"
>YunoHost</a
>
for accessible self-hosting.
</p>
</div>
<div class="w3-container w3-cell w3-mobile">
<h3>Desktop</h3>
<p>
<a
class="w3-button w3-round-large w3-black w3-padding-large"
href="https://dev.tildefriends.net/cory/tildefriends/releases"
><i class="fa fa-download"></i> Download</a
>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray"
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
</a>
</p>
<p>
Tilde Friends is distributed as a single executable file (or
source that you can
<a href="http://dev.tildefriends.net">build yourself</a>)
and stores all of its data in a single
file(<code>db.sqlite</code>). You can generally download the
latest executable from
<a
href="https://dev.tildefriends.net/cory/tildefriends/releases"
>releases</a
>
for your platform, mark it as executable (<code
>chmod +x tildefriends*</code
>
on macOS and Linux), and run. Run with <code>-h</code> to
learn more.
</p>
<p>
Tilde Friends will run in the console and provide a web
interface at
<a href="http://localhost:12345/">http://localhost:12345/</a
>. You will have to register a username and password to sign
into your instance.
</p>
</div>
</div>
<p>
After a <a href="/~core/intro">brief introduction</a>, Tilde
Friends will take you to the Secure Scuttlebutt social network
app.
</p>
</li>
<li>Create an account to identify yourself with that instance.</li>
<li>
Describe yourself in your profile in the <b>ssb</b> app. Give
yourself a name and an avatar if you like.
@@ -158,11 +224,11 @@
<!-- SSB Section -->
<div class="w3-light-grey">
<div class="w3-row-padding w3-padding-64">
<div class="w3-col l4 m6 s4">
<div class="w3-col l4 m6 s4 w3-center">
<a href="https://scuttlebutt.nz/"
><img
class="w3-image w3-round-large"
src="ssb.png"
class="w3-image"
src="hermietildefriends.svg"
alt="Secure Scuttlebutt"
/></a>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

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,7 +1,23 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js';
let gSessionIndex = 0;
export {App};
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** App constructor.
** @return An app instance.
*/
function App() {
this._send_queue = [];
this.calls = {};
@@ -9,6 +25,12 @@ function App() {
return this;
}
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
App.prototype.makeFunction = function (api) {
let self = this;
let result = function () {
@@ -32,6 +54,10 @@ App.prototype.makeFunction = function (api) {
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
App.prototype.send = function (message) {
if (this._send_queue) {
if (this._on_output) {
@@ -46,6 +72,11 @@ App.prototype.send = function (message) {
}
};
/**
** App socket handler.
** @param request The HTTP request of the WebSocket connection.
** @param response The HTTP response.
*/
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
@@ -133,7 +164,7 @@ exports.app_socket = async function socket(request, response) {
options.packageOwner = packageOwner;
options.packageName = packageName;
options.url = message.url;
let sessionId = 'session_' + (gSessionIndex++).toString();
let sessionId = 'session_' + (g_session_index++).toString();
if (blobId) {
if (message.edit_only) {
response.send(
@@ -218,4 +249,4 @@ exports.app_socket = async function socket(request, response) {
response.upgrade(100, {});
};
export {App};
/** @} */

View File

@@ -1,3 +1,11 @@
/**
* \file
* \defgroup tfclient Tilde Friends Client JS
* Tilde Friends client-side browser JavaScript.
* @{
*/
/** \cond */
import {LitElement, html, css, svg} from '/lit/lit-all.min.js';
let cm6;
@@ -8,12 +16,14 @@ let gFiles = {};
let gApp = {files: {}, emoji: '📦'};
let gEditor;
let gOriginalInput;
let gUnloading;
let kErrorColor = '#dc322f';
let kDisconnectColor = '#f00';
let kStatusColor = '#fff';
/** \endcond */
// Functions that server-side app code can call through the app object.
/** Functions that server-side app code can call through the app object. */
const k_api = {
setDocument: {args: ['content'], func: api_setDocument},
postMessage: {args: ['message'], func: api_postMessage},
@@ -25,29 +35,14 @@ const k_api = {
setHash: {args: ['hash'], func: api_setHash},
};
// TODO(tasiaiso): this is only used once, move it down ?
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
/**
* Class that represents the top bar
*/
class TfNavigationElement extends LitElement {
/**
* Get Lit Html properties.
* @return The properties.
*/
static get properties() {
return {
credentials: {type: Object},
@@ -63,6 +58,9 @@ class TfNavigationElement extends LitElement {
};
}
/**
* Create a TfNavigationElement instance.
*/
constructor() {
super();
this.permissions = {};
@@ -74,8 +72,8 @@ class TfNavigationElement extends LitElement {
}
/**
* TODOC
* @param {*} event
* Toggle editor visibility.
* @param event The HTML event.
*/
toggle_edit(event) {
event.preventDefault();
@@ -87,18 +85,18 @@ class TfNavigationElement extends LitElement {
}
/**
* TODOC
* @param {*} key
* Remove a stored permission.
* @param key The permission to reset.
*/
reset_permission(key) {
send({action: 'resetPermission', permission: key});
}
/**
* TODOC
* @param {*} key
* @param {*} options
* @returns
* Get or create a spark line.
* @param key The spark line identifier.
* @param options Spark line options.
* @return A spark line HTML element.
*/
get_spark_line(key, options) {
if (!this.spark_lines[key]) {
@@ -117,29 +115,49 @@ class TfNavigationElement extends LitElement {
return this.spark_lines[key];
}
/**
* Set the active SSB identity for the current application.
* @param id The identity.
*/
set_active_identity(id) {
send({action: 'setActiveIdentity', identity: id});
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
}
create_identity(event) {
/**
* Create a new SSB identity.
*/
create_identity() {
if (confirm('Are you sure you want to create a new identity?')) {
send({action: 'createIdentity'});
}
}
/**
* Toggle visibility of the ID dropdown.
*/
toggle_id_dropdown() {
this.renderRoot.getElementById('id_dropdown').classList.toggle('w3-show');
}
/**
* Edit the current identity's SSB profile.
*/
edit_profile() {
window.location.href = '/~core/ssb/#' + this.identity;
}
/**
* Sign out of the current Tilde Friends user.
*/
logout() {
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
}
/**
* Render the identity dropdown.
* @return Lit HTML.
*/
render_identity() {
let self = this;
@@ -244,8 +262,8 @@ class TfNavigationElement extends LitElement {
}
/**
* TODOC
* @returns
* Render the permissions popup.
* @return Lit HTML.
*/
render_permissions() {
if (this.show_permissions) {
@@ -286,16 +304,36 @@ class TfNavigationElement extends LitElement {
}
}
/**
* Clear the current error.
*/
clear_error() {
this.status = {};
}
/**
* TODOC
* @returns
* Render the navigation bar.
* @return Lit HTML.
*/
render() {
let self = this;
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<style>
@@ -403,9 +441,13 @@ class TfNavigationElement extends LitElement {
customElements.define('tf-navigation', TfNavigationElement);
/**
* TODOC
* A file in the files sidebar.
*/
class TfFilesElement extends LitElement {
/**
* LitElement properties.
* @return The properties.
*/
static get properties() {
return {
current: {type: String},
@@ -415,6 +457,9 @@ class TfFilesElement extends LitElement {
};
}
/**
* Create a TfFilesElement instance.
*/
constructor() {
super();
this.files = {};
@@ -422,8 +467,8 @@ class TfFilesElement extends LitElement {
}
/**
* TODOC
* @param {*} file
* Select a clicked file.
* @param file The file.
*/
file_click(file) {
this.dispatchEvent(
@@ -438,9 +483,9 @@ class TfFilesElement extends LitElement {
}
/**
* TODOC
* @param {*} file
* @returns
* Render a single file in the file list.
* @param file The file.
* @return Lit HTML.
*/
render_file(file) {
let classes = ['file'];
@@ -462,8 +507,8 @@ class TfFilesElement extends LitElement {
}
/**
* TODOC
* @param {*} event
* Create a file entry for a dropped file.
* @param event The event.
*/
async drop(event) {
event.preventDefault();
@@ -488,8 +533,8 @@ class TfFilesElement extends LitElement {
}
/**
* TODOC
* @param {*} event
* Called when a file starts being dragged over the file.
* @param event The event.
*/
drag_enter(event) {
this.dropping++;
@@ -498,8 +543,8 @@ class TfFilesElement extends LitElement {
}
/**
* TODOC
* @param {*} event
* Called when a file stops being dragged over the file.
* @param event The event.
*/
drag_leave(event) {
this.dropping--;
@@ -508,13 +553,17 @@ class TfFilesElement extends LitElement {
}
}
/**
* Called when a file is being dragged over the file.
* @param event The event.
*/
drag_over(event) {
event.preventDefault();
}
/**
* TODOC
* @returns
* Render the file.
* @return Lit HTML.
*/
render() {
let self = this;
@@ -561,9 +610,13 @@ class TfFilesElement extends LitElement {
customElements.define('tf-files', TfFilesElement);
/**
* TODOC
* The files pane element.
*/
class TfFilesPaneElement extends LitElement {
/**
* Get Lit Html properties.
* @return The properties.
*/
static get properties() {
return {
expanded: {type: Boolean},
@@ -572,6 +625,9 @@ class TfFilesPaneElement extends LitElement {
};
}
/**
* Create a TfFilesPaneElement instance.
*/
constructor() {
super();
this.expanded = window.localStorage.getItem('files') != '0';
@@ -579,8 +635,8 @@ class TfFilesPaneElement extends LitElement {
}
/**
* TODOC
* @param {*} expanded
* Set whether the files pane is expanded.
* @param expanded Whether the files pane is expanded.
*/
set_expanded(expanded) {
this.expanded = expanded;
@@ -588,8 +644,8 @@ class TfFilesPaneElement extends LitElement {
}
/**
* TODOC
* @returns
* Render the files pane element.
* @return Lit HTML.
*/
render() {
let self = this;
@@ -648,7 +704,7 @@ class TfFilesPaneElement extends LitElement {
customElements.define('tf-files-pane', TfFilesPaneElement);
/**
* TODOC
* A tiny graph.
*/
class TfSparkLineElement extends LitElement {
static get properties() {
@@ -668,9 +724,9 @@ class TfSparkLineElement extends LitElement {
}
/**
* TODOC
* @param {*} key
* @param {*} value
* Add a data point to the graph.
* @param key The line to which the point applies.
* @param value The numeric value of the data point.
*/
append(key, value) {
let line = null;
@@ -697,9 +753,9 @@ class TfSparkLineElement extends LitElement {
}
/**
* TODOC
* @param {*} line
* @returns
* Render a single series line.
* @param line The line data.
* @return Lit HTML.
*/
render_line(line) {
if (line?.values?.length >= 2) {
@@ -715,8 +771,8 @@ class TfSparkLineElement extends LitElement {
}
/**
* TODOC
* @returns
* Render the graph.
* @return Lit HTML.
*/
render() {
let max =
@@ -743,7 +799,9 @@ class TfSparkLineElement extends LitElement {
customElements.define('tf-sparkline', TfSparkLineElement);
// TODOC
/**
* A keyboard key is pressed down.
*/
window.addEventListener('keydown', function (event) {
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
if (editing()) {
@@ -759,10 +817,9 @@ window.addEventListener('keydown', function (event) {
});
/**
* TODOC
* @param {*} nodes
* @param {*} callback
* @returns
* Make sure a set of dependencies are loaded
* @param nodes An array of descriptions of dependencies to load.
* @param callback Called when all dependencies are loaded.
*/
function ensureLoaded(nodes, callback) {
if (!nodes.length) {
@@ -805,24 +862,23 @@ function ensureLoaded(nodes, callback) {
}
/**
* TODOC
* @returns
* Check whether the editior is currently visible.
* @return true if the editor is visible.
*/
function editing() {
return document.getElementById('editPane').style.display != 'none';
}
/**
* TODOC
* @returns
* Check whether only the editor is visible and the app is hidden.
* @return true if the editor is visible and the app is not.
*/
function is_edit_only() {
return window.location.search == '?editonly=1' || window.innerWidth < 1024;
}
/**
* TODOC
* @returns
* Show the editor.
*/
async function edit() {
if (editing()) {
@@ -849,30 +905,17 @@ async function edit() {
}
/**
* TODOC
* Open a performance trace.
*/
function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
}
/**
* TODOC
* @param {*} name
* @returns
*/
function guessMode(name) {
return name.endsWith('.js')
? 'javascript'
: name.endsWith('.html')
? 'htmlmixed'
: null;
}
/**
* TODOC
* @param {*} name
* @param {*} id
* @returns
* Load a single file.
* @param name The name by which the file is known.
* @param id The file's ID.
* @return A promise resolved with the file's contents.
*/
function loadFile(name, id) {
return fetch('/' + id + '/view')
@@ -898,9 +941,9 @@ function loadFile(name, id) {
}
/**
* TODOC
* @param {*} path
* @returns
* Load files for the app.
* @param path The app path to load.
* @return A promise resolved when the app is laoded.
*/
async function load(path) {
let response = await fetch((path || url()) + 'view');
@@ -940,7 +983,7 @@ async function load(path) {
}
/**
* TODOC
* Hide the editor.
*/
function closeEditor() {
window.localStorage.setItem('editing', '0');
@@ -949,17 +992,9 @@ function closeEditor() {
}
/**
* TODOC
* @returns
*/
function explodePath() {
return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname);
}
/**
* TODOC
* @param {*} save_to
* @returns
* Save the app.
* @param save_to An optional path to which to save the app.
* @return A promise resoled when the app is saved.
*/
function save(save_to) {
document.getElementById('save').disabled = true;
@@ -1049,6 +1084,8 @@ function save(save_to) {
if (save_path != window.location.pathname) {
alert('Saved to ' + save_path + '.');
} else if (!gFiles['app.js']) {
window.location.reload();
} else {
reconnect(save_path);
}
@@ -1067,7 +1104,7 @@ function save(save_to) {
}
/**
* TODOC
* Prompt to set the app icon.
*/
function changeIcon() {
let value = prompt('Enter a new app icon emoji:');
@@ -1078,7 +1115,7 @@ function changeIcon() {
}
/**
* TODOC
* Prompt to delete the current app.
*/
function deleteApp() {
let name = document.getElementById('name');
@@ -1099,8 +1136,8 @@ function deleteApp() {
}
/**
* TODOC
* @returns
* Get the current app URL.
* @return The app URL.
*/
function url() {
let hash = window.location.href.indexOf('#');
@@ -1118,16 +1155,16 @@ function url() {
}
/**
* TODOC
* @returns
* Get the window hash without the lone '#' if it is empty.
* @return The hash.
*/
function hash() {
return window.location.hash != '#' ? window.location.hash : '';
}
/**
* TODOC
* @param {*} content
* Set the iframe document contents.
* @param content The contents.
*/
function api_setDocument(content) {
let iframe = document.getElementById('document');
@@ -1135,8 +1172,8 @@ function api_setDocument(content) {
}
/**
* TODOC
* @param {*} message
* Send a message to the sandboxed iframe.
* @param message The message.
*/
function api_postMessage(message) {
let iframe = document.getElementById('document');
@@ -1144,8 +1181,8 @@ function api_postMessage(message) {
}
/**
* TODOC
* @param {*} error
* Show an error.
* @param error The error.
*/
function api_error(error) {
if (error) {
@@ -1159,28 +1196,28 @@ function api_error(error) {
}
/**
* TODOC
* @param {*} key
* @param {*} value
et a value in local storage.
* @param key The key.
* @param value The value.
*/
function api_localStorageSet(key, value) {
window.localStorage.setItem('app:' + key, value);
}
/**
* TODOC
* @param {*} key
* @returns
* Get a value from local storage.
* @param key The key.
* @return The value.
*/
function api_localStorageGet(key) {
return window.localStorage.getItem('app:' + key);
}
/**
* TODOC
* @param {*} permission
* @param {*} id
* @returns
* Request a permission
* @param permission The permission to request.
* @param id The id requeesting the permission.
* @return A promise fulfilled if the permission was granted.
*/
function api_requestPermission(permission, id) {
let outer = document.createElement('div');
@@ -1249,23 +1286,23 @@ function api_requestPermission(permission, id) {
}
/**
* TODOC
* Log from the app to the console.
*/
function api_print() {
console.log('app>', ...arguments);
}
/**
* TODOC
* @param {*} hash
* Set the window's location hash.
* @param hash The new hash.
*/
function api_setHash(hash) {
window.location.hash = hash;
}
/**
* TODOC
* @param {*} message
* Process an incoming WebSocket message.
* @param message The message.
*/
function _receive_websocket_message(message) {
if (message && message.action == 'session') {
@@ -1361,9 +1398,9 @@ function _receive_websocket_message(message) {
}
/**
* TODOC
* @param {*} message
* @param {*} color
* Set the status message.
* @param message The message.
* @param color The message's color.
*/
function setStatusMessage(message, color) {
document.getElementsByTagName('tf-navigation')[0].status = {
@@ -1374,8 +1411,8 @@ function setStatusMessage(message, color) {
}
/**
* TODOC
* @param {*} value
* Send a message to the app.
* @param value The message.
*/
function send(value) {
try {
@@ -1388,57 +1425,14 @@ function send(value) {
}
/**
* TODOC
* @param {*} sourceData
* @param {*} maxWidth
* @param {*} maxHeight
* @param {*} callback
*/
function fixImage(sourceData, maxWidth, maxHeight, callback) {
let result = sourceData;
let image = new Image();
image.crossOrigin = 'anonymous';
image.referrerPolicy = 'no-referrer';
image.onload = function () {
if (image.width > maxWidth || image.height > maxHeight) {
let downScale = Math.min(
maxWidth / image.width,
maxHeight / image.height
);
let canvas = document.createElement('canvas');
canvas.width = image.width * downScale;
canvas.height = image.height * downScale;
let context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
image.width = canvas.width;
image.height = canvas.height;
context.drawImage(image, 0, 0, image.width, image.height);
result = canvas.toDataURL();
}
callback(result);
};
image.src = sourceData;
}
/**
* TODOC
* @param {*} image
*/
function sendImage(image) {
fixImage(image, 320, 240, function (result) {
send({image: result});
});
}
/**
* TODOC
* Notify the app of the window hash changing.
*/
function hashChange() {
send({event: 'hashChange', hash: window.location.hash});
}
/**
* TODOC
* Make sure the app is connected on window focus, and notify the app.
*/
function focus() {
if (gSocket && gSocket.readyState == gSocket.CLOSED) {
@@ -1449,7 +1443,7 @@ function focus() {
}
/**
* TODOC
* Notify the app of lost focus.
*/
function blur() {
if (gSocket && gSocket.readyState == gSocket.OPEN) {
@@ -1458,8 +1452,8 @@ function blur() {
}
/**
* TODOC
* @param {*} event
* Handle a message.
* @param event The message.
*/
function message(event) {
if (
@@ -1507,8 +1501,8 @@ function message(event) {
}
/**
* TODOC
* @param {*} path
* Reconnect the WebSocket.
* @param path The path to which the WebSocket should be connected.
*/
function reconnect(path) {
let oldSocket = gSocket;
@@ -1523,8 +1517,8 @@ function reconnect(path) {
}
/**
* TODOC
* @param {*} path
* Connect the WebSocket.
* @param path The path to which to connect.
*/
function connectSocket(path) {
if (!gSocket || gSocket.readyState != gSocket.OPEN) {
@@ -1560,34 +1554,38 @@ function connectSocket(path) {
_receive_websocket_message(JSON.parse(event.data));
};
gSocket.onclose = function (event) {
const k_codes = {
1000: 'Normal closure',
1001: 'Going away',
1002: 'Protocol error',
1003: 'Unsupported data',
1005: 'No status received',
1006: 'Abnormal closure',
1007: 'Invalid frame payload data',
1008: 'Policy violation',
1009: 'Message too big',
1010: 'Missing extension',
1011: 'Internal error',
1012: 'Service restart',
1013: 'Try again later',
1014: 'Bad gateway',
1015: 'TLS handshake',
};
setStatusMessage(
'🔴 Closed: ' + (k_codes[event.code] || event.code),
kDisconnectColor
);
if (gUnloading) {
setStatusMessage('⚪ Closing...', kStatusColor);
} else {
const k_codes = {
1000: 'Normal closure',
1001: 'Going away',
1002: 'Protocol error',
1003: 'Unsupported data',
1005: 'No status received',
1006: 'Abnormal closure',
1007: 'Invalid frame payload data',
1008: 'Policy violation',
1009: 'Message too big',
1010: 'Missing extension',
1011: 'Internal error',
1012: 'Service restart',
1013: 'Try again later',
1014: 'Bad gateway',
1015: 'TLS handshake',
};
setStatusMessage(
'🔴 Closed: ' + (k_codes[event.code] || event.code),
kDisconnectColor
);
}
};
}
}
/**
* TODOC
* @param {*} name
* Open a file by name.
* @param name The file to open.
*/
function openFile(name) {
let newDoc =
@@ -1612,7 +1610,7 @@ function openFile(name) {
}
/**
* TODOC
* Refresh the files list.
*/
function updateFiles() {
let files = document.getElementsByTagName('tf-files-pane')[0];
@@ -1634,8 +1632,8 @@ function updateFiles() {
}
/**
* TODOC
* @param {*} name
* Create a new file with the given name.
* @param name The file's name.
*/
function makeNewFile(name) {
gFiles[name] = {
@@ -1645,7 +1643,7 @@ function makeNewFile(name) {
}
/**
* TODOC
* Prompt to create a new file.
*/
function newFile() {
let name = prompt('Name of new file:', 'file.js');
@@ -1655,7 +1653,7 @@ function newFile() {
}
/**
* TODOC
* Prompt to remove a file.
*/
function removeFile() {
if (confirm('Remove ' + gCurrentFile + '?')) {
@@ -1665,7 +1663,7 @@ function removeFile() {
}
/**
* TODOC
* Export the app to a zip file, which is downloaded by the browser.
*/
async function appExport() {
let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1696,10 +1694,10 @@ async function appExport() {
}
/**
* TODOC
* @param {*} name
* @param {*} file
* @returns
* Save a file.
* @param name The file to svae.
* @param file The file contents.
* @return A promise resolved with the blob ID of the saved file.
*/
async function save_file_to_blob_id(name, file) {
console.log(`Saving ${name}.`);
@@ -1723,7 +1721,7 @@ async function save_file_to_blob_id(name, file) {
}
/**
* TODOC
* Prompt to import an app from a zip file.
*/
async function appImport() {
let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1794,7 +1792,7 @@ async function appImport() {
}
/**
*
* Prettify the current source file.
*/
async function sourcePretty() {
let prettier = (await import('/prettier/standalone.mjs')).default;
@@ -1822,6 +1820,9 @@ async function sourcePretty() {
}
}
/**
* Toggle visible whitespace.
*/
function toggleVisibleWhitespace() {
let editor_style = document.getElementById('editor_style');
/*
@@ -1847,13 +1848,18 @@ function toggleVisibleWhitespace() {
}
}
// TODOC
/**
* Register event handlers and connect the WebSocket on load.
*/
window.addEventListener('load', function () {
window.addEventListener('hashchange', hashChange);
window.addEventListener('focus', focus);
window.addEventListener('blur', blur);
window.addEventListener('message', message, false);
window.addEventListener('online', connectSocket);
window.addEventListener('beforeunload', function () {
gUnloading = true;
});
document.getElementById('name').value = window.location.pathname;
document
.getElementById('closeEditor')
@@ -1892,3 +1898,5 @@ window.addEventListener('load', function () {
toggleVisibleWhitespace();
}
});
/** @} */

View File

@@ -1,12 +1,30 @@
/**
* \file
* \defgroup tfcore Tilde Friends Core JS
* Tilde Friends process management, in JavaScript.
* @{
*/
/** \cond */
import * as app from './app.js';
import * as http from './http.js';
let gProcesses = {};
let gStatsTimer = false;
let g_handler_index = 0;
export {invoke, getProcessBlob};
/** \endcond */
/** All running processes. */
let gProcesses = {};
/** Whether stats are currently being sent. */
let gStatsTimer = false;
/** Effectively a process ID. */
let g_handler_index = 0;
/** Time between pings, in milliseconds. */
const k_ping_interval = 60 * 1000;
/**
* Print an error.
* @param error The error.
*/
function printError(error) {
if (error.stackTrace) {
print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
@@ -19,6 +37,12 @@ function printError(error) {
}
}
/**
* Invoke a handler.
* @param handlers The handlers on which to invoke the callback.
* @param argv Arguments to pass to the handlers.
* @return A promise.
*/
function invoke(handlers, argv) {
let promises = [];
if (handlers) {
@@ -39,6 +63,12 @@ function invoke(handlers, argv) {
return Promise.all(promises);
}
/**
* Broadcast a named event to all registered apps.
* @param eventName the name of the event.
* @param argv Arguments to pass to the handlers.
* @return A promise.
*/
function broadcastEvent(eventName, argv) {
let promises = [];
for (let process of Object.values(gProcesses)) {
@@ -49,6 +79,11 @@ function broadcastEvent(eventName, argv) {
return Promise.all(promises);
}
/**
* Send a message to all other instances of the same app.
* @param message The message.
* @return A promise.
*/
function broadcast(message) {
let sender = this;
let promises = [];
@@ -65,6 +100,15 @@ function broadcast(message) {
return Promise.all(promises);
}
/**
* Send a message to all instances of the same app running as the same user.
* @param user The user.
* @param packageOwner The owner of the app.
* @param packageName The name of the app.
* @param eventName The name of the event.
* @param argv The arguments to pass.
* @return A promise.
*/
function broadcastAppEventToUser(
user,
packageOwner,
@@ -87,6 +131,11 @@ function broadcastAppEventToUser(
return Promise.all(promises);
}
/**
* Get user context information for a call.
* @param caller The calling process.
* @param process The receiving process.
*/
function getUser(caller, process) {
return {
key: process.key,
@@ -97,38 +146,26 @@ function getUser(caller, process) {
};
}
async function getApps(user, process) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
if (user && user !== process.credentials.session.name && user !== 'core') {
return {};
} else if (!user) {
user = process.credentials.session.name;
}
}
if (user) {
let db = new Database(user);
try {
let names = JSON.parse(await db.get('apps'));
let result = {};
for (let name of names) {
result[name] = await db.get('path:' + name);
}
return result;
} catch {}
}
return {};
}
/**
* Send a message.
* @param from The calling process.
* @param to The receiving process.
* @param message The message.
* @return A promise.
*/
function postMessageInternal(from, to, message) {
if (to.eventHandlers['message']) {
return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
}
}
/**
* Get or create a process for an app blob.
* @param blobId The blob identifier.
* @param key A unique key for the invocation.
* @param options Other options.
* @return The process.
*/
async function getProcessBlob(blobId, key, options) {
let process = gProcesses[key];
if (!process && !(options && 'create' in options && !options.create)) {
@@ -220,7 +257,6 @@ async function getProcessBlob(blobId, key, options) {
let settings = await loadSettings();
return settings?.permissions?.[user] ?? [];
},
apps: (user) => getApps(user, process),
getSockets: getSockets,
permissionTest: async function (permission) {
let user = process?.credentials?.session?.name;
@@ -679,6 +715,10 @@ ssb.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []);
});
/**
* Load settings from the database.
* @return The settings as a key value pairs object.
*/
async function loadSettings() {
let data = {};
try {
@@ -697,6 +737,9 @@ async function loadSettings() {
return data;
}
/**
* Send periodic stats to all clients.
*/
function sendStats() {
let apps = Object.values(gProcesses)
.filter((process) => process.app)
@@ -712,6 +755,16 @@ function sendStats() {
}
}
/**
* Invoke an app's handler.js.
* @param response The response object.
* @param app_blob_id The app's blob identifier.
* @param path The request path.
* @param query The request query string.
* @param headers The request headers.
* @param package_owner The app's owner.
* @param package_name The app's name.
*/
exports.callAppHandler = async function callAppHandler(
response,
app_blob_id,
@@ -777,4 +830,4 @@ exports.callAppHandler = async function callAppHandler(
response.end(answer?.data);
};
export {invoke, getProcessBlob};
/** @} */

View File

@@ -1,8 +1,14 @@
/**
* TODOC
* TODO: document so we can improve this
* @param {*} url
* @returns
* \file
* \defgroup tfhttp Tilde Friends HTTP Client JS
* Tilde Friends server-side HTTP client.
* @{
*/
/**
* Parse a URL into protocol, host, path, and port parts.
* @param url
* @return An object of the URL parts.
*/
function parseUrl(url) {
// XXX: Hack.
@@ -16,9 +22,9 @@ function parseUrl(url) {
}
/**
* TODOC
* @param {*} data
* @returns
* Parse an HTTP response into headers and body content.
* @param data The response data, headers and body included.
* @return headers and body data.
*/
function parseResponse(data) {
let firstLine;
@@ -36,15 +42,15 @@ function parseResponse(data) {
headers[line.substring(colon)] = line.substring(colon + 1);
}
}
return {body: data};
return {headers: headers, body: data};
}
/**
* TODOC
* @param {*} url
* @param {*} options
* @param {*} allowed_hosts
* @returns
* Make an HTTP request.
* @param url The URL.
* @param options Request options.
* @param allowed_hosts List of allowed hosts.
* @return A promise resolved with the response headers and body.
*/
export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url);
@@ -111,3 +117,5 @@ export function fetch(url, options, allowed_hosts) {
});
});
}
/** @} */

View File

@@ -1,11 +1,22 @@
/**
* \file
* \defgroup tfrpc Tilde Friends RPC.
* Tilde Friends RPC.
* @{
*/
/** Whether this module is being run in a web browser. */
const k_is_browser = get_is_browser();
/** Registered methods. */
let g_api = {};
/** The next method identifier. */
let g_next_id = 1;
/** Identifiers of pending calls. */
let g_calls = {};
/**
* TODOC
* @returns
* Check if being called from a browser vs. server-side.
* @return true if called from a browser.
*/
function get_is_browser() {
try {
@@ -15,16 +26,30 @@ function get_is_browser() {
}
}
/** \cond */
if (k_is_browser) {
print = console.log;
}
if (k_is_browser) {
window.addEventListener('message', function (event) {
call_rpc(event.data);
});
} else {
core.register('message', function (message) {
call_rpc(message?.message);
});
}
export let rpc = new Proxy({}, {get: make_rpc});
/** \endcond */
/**
* TODOC
* @param {*} target
* @param {*} prop
* @param {*} receiver
* @returns
* Make a function to invoke a remote procedure.
* @param target The target.
* @param prop The name of the function.
* @param receiver The receiver.
* @return A function.
*/
function make_rpc(target, prop, receiver) {
return function () {
@@ -55,8 +80,8 @@ function make_rpc(target, prop, receiver) {
}
/**
* TODOC
* @param {*} response
* Send a response.
* @param response The response.
*/
function send(response) {
if (k_is_browser) {
@@ -67,8 +92,8 @@ function send(response) {
}
/**
* TODOC
* @param {*} message
* Invoke a remote procedure.
* @param message An object describing the call.
*/
function call_rpc(message) {
if (message && message.message === 'tfrpc') {
@@ -112,22 +137,12 @@ function call_rpc(message) {
}
}
if (k_is_browser) {
window.addEventListener('message', function (event) {
call_rpc(event.data);
});
} else {
core.register('message', function (message) {
call_rpc(message?.message);
});
}
export let rpc = new Proxy({}, {get: make_rpc});
/**
* TODOC
* @param {*} method
* Register a function that to be called remotely.
* @param method The method.
*/
export function register(method) {
g_api[method.name] = method;
}
/** @} */

View File

@@ -25,14 +25,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.30";
version = "0.0.33";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-t5yvouzSL2j/ge1VHLqzIZ+Avqj4iEDt7L+yrHoTZAQ=";
hash = "sha256-9D28gmaBTRVyXhY3zZd/W9PsXA1YZt/K69hz41aVP04=";
fetchSubmodules = true;
};

File diff suppressed because one or more lines are too long

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

@@ -22,6 +22,7 @@
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -33,6 +34,7 @@
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
@@ -44,6 +46,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@@ -56,6 +59,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -72,6 +76,7 @@
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@@ -83,18 +88,20 @@
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -108,6 +115,7 @@
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
@@ -118,6 +126,7 @@
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@@ -128,14 +137,16 @@
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -144,27 +155,26 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.36.8",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.8.tgz",
"integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==",
"version": "6.38.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
"integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
"integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -172,40 +182,35 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"version": "0.3.10",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
"integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
"integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -214,12 +219,14 @@
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.2.1.tgz",
"integrity": "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
"integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@@ -230,6 +237,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@@ -238,6 +246,7 @@
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@@ -248,6 +257,7 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@@ -258,6 +268,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@@ -268,6 +279,7 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@@ -275,12 +287,14 @@
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@@ -305,6 +319,7 @@
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"serialize-javascript": "^6.0.1",
"smob": "^1.0.0",
@@ -323,9 +338,10 @@
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@@ -344,260 +360,283 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
"integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.0.tgz",
"integrity": "sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
"integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.0.tgz",
"integrity": "sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
"integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.0.tgz",
"integrity": "sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
"integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.0.tgz",
"integrity": "sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
"integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.0.tgz",
"integrity": "sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
"integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.0.tgz",
"integrity": "sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
"integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.0.tgz",
"integrity": "sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
"integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.0.tgz",
"integrity": "sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
"integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.0.tgz",
"integrity": "sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
"integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.0.tgz",
"integrity": "sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
"integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.0.tgz",
"integrity": "sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
"integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.0.tgz",
"integrity": "sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
"integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.0.tgz",
"integrity": "sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
"integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.0.tgz",
"integrity": "sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
"integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.0.tgz",
"integrity": "sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
"integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.0.tgz",
"integrity": "sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
"integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.0.tgz",
"integrity": "sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
"integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.0.tgz",
"integrity": "sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
"integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.0.tgz",
"integrity": "sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
"integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.0.tgz",
"integrity": "sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -609,12 +648,14 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@@ -629,17 +670,20 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -647,13 +691,15 @@
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -666,6 +712,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -674,6 +721,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -685,6 +733,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
@@ -698,17 +747,20 @@
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": {
"node": ">=12"
},
@@ -721,6 +773,7 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
@@ -729,6 +782,7 @@
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
@@ -745,11 +799,12 @@
}
},
"node_modules/rollup": {
"version": "4.41.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
"integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.0.tgz",
"integrity": "sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.7"
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -759,26 +814,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.41.1",
"@rollup/rollup-android-arm64": "4.41.1",
"@rollup/rollup-darwin-arm64": "4.41.1",
"@rollup/rollup-darwin-x64": "4.41.1",
"@rollup/rollup-freebsd-arm64": "4.41.1",
"@rollup/rollup-freebsd-x64": "4.41.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
"@rollup/rollup-linux-arm-musleabihf": "4.41.1",
"@rollup/rollup-linux-arm64-gnu": "4.41.1",
"@rollup/rollup-linux-arm64-musl": "4.41.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
"@rollup/rollup-linux-riscv64-gnu": "4.41.1",
"@rollup/rollup-linux-riscv64-musl": "4.41.1",
"@rollup/rollup-linux-s390x-gnu": "4.41.1",
"@rollup/rollup-linux-x64-gnu": "4.41.1",
"@rollup/rollup-linux-x64-musl": "4.41.1",
"@rollup/rollup-win32-arm64-msvc": "4.41.1",
"@rollup/rollup-win32-ia32-msvc": "4.41.1",
"@rollup/rollup-win32-x64-msvc": "4.41.1",
"@rollup/rollup-android-arm-eabi": "4.46.0",
"@rollup/rollup-android-arm64": "4.46.0",
"@rollup/rollup-darwin-arm64": "4.46.0",
"@rollup/rollup-darwin-x64": "4.46.0",
"@rollup/rollup-freebsd-arm64": "4.46.0",
"@rollup/rollup-freebsd-x64": "4.46.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.0",
"@rollup/rollup-linux-arm-musleabihf": "4.46.0",
"@rollup/rollup-linux-arm64-gnu": "4.46.0",
"@rollup/rollup-linux-arm64-musl": "4.46.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.0",
"@rollup/rollup-linux-ppc64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-musl": "4.46.0",
"@rollup/rollup-linux-s390x-gnu": "4.46.0",
"@rollup/rollup-linux-x64-gnu": "4.46.0",
"@rollup/rollup-linux-x64-musl": "4.46.0",
"@rollup/rollup-win32-arm64-msvc": "4.46.0",
"@rollup/rollup-win32-ia32-msvc": "4.46.0",
"@rollup/rollup-win32-x64-msvc": "4.46.0",
"fsevents": "~2.3.2"
}
},
@@ -800,13 +855,15 @@
"type": "consulting",
"url": "https://feross.org/support"
}
]
],
"license": "MIT"
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
@@ -815,13 +872,15 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -831,6 +890,7 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -839,12 +899,14 @@
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -853,10 +915,11 @@
}
},
"node_modules/terser": {
"version": "5.40.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz",
"integrity": "sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==",
"version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0",
@@ -873,7 +936,8 @@
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head>
<body>
<script src="speedscope-VHEG2FVF.js"></script>
<script src="speedscope-HCR63FMT.js"></script>

View File

@@ -1,3 +1,3 @@
speedscope@1.22.2
Sat Feb 15 13:02:38 PST 2025
1c254dcb3e2b4f6d921340d20e972d9d27b788f4
speedscope@1.23.1
Mon Aug 11 11:43:09 PDT 2025
0cec0f82c334aed6cf19d43cabeadcda0d95e0fc

File diff suppressed because one or more lines are too long

966
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

4656
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

316
deps/sqlite/sqlite3.h vendored
View File

@@ -133,7 +133,7 @@ extern "C" {
**
** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the
** <a href="http://www.fossil-scm.org/">Fossil configuration management
** <a href="http://fossil-scm.org/">Fossil configuration management
** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite
** within its configuration management system. ^The SQLITE_SOURCE_ID
@@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.49.2"
#define SQLITE_VERSION_NUMBER 3049002
#define SQLITE_SOURCE_ID "2025-05-07 10:39:52 17144570b0d96ae63cd6f3edca39e27ebd74925252bbaf6723bcb2f6b4861fb1"
#define SQLITE_VERSION "3.50.4"
#define SQLITE_VERSION_NUMBER 3050004
#define SQLITE_SOURCE_ID "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
/*
** CAPI3REF: Run-Time Library Version Numbers
@@ -1163,6 +1163,12 @@ struct sqlite3_io_methods {
** the value that M is to be set to. Before returning, the 32-bit signed
** integer is overwritten with the previous value of M.
**
** <li>[[SQLITE_FCNTL_BLOCK_ON_CONNECT]]
** The [SQLITE_FCNTL_BLOCK_ON_CONNECT] opcode is used to configure the
** VFS to block when taking a SHARED lock to connect to a wal mode database.
** This is used to implement the functionality associated with
** SQLITE_SETLK_BLOCK_ON_CONNECT.
**
** <li>[[SQLITE_FCNTL_DATA_VERSION]]
** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to
** a database file. The argument is a pointer to a 32-bit unsigned integer.
@@ -1259,6 +1265,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_CKSM_FILE 41
#define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43
#define SQLITE_FCNTL_BLOCK_ON_CONNECT 44
/* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@@ -1989,13 +1996,16 @@ struct sqlite3_mem_methods {
**
** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt>
** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine
** the default size of lookaside memory on each [database connection].
** the default size of [lookaside memory] on each [database connection].
** The first argument is the
** size of each lookaside buffer slot and the second is the number of
** slots allocated to each database connection.)^ ^(SQLITE_CONFIG_LOOKASIDE
** sets the <i>default</i> lookaside size. The [SQLITE_DBCONFIG_LOOKASIDE]
** option to [sqlite3_db_config()] can be used to change the lookaside
** configuration on individual connections.)^ </dd>
** size of each lookaside buffer slot ("sz") and the second is the number of
** slots allocated to each database connection ("cnt").)^
** ^(SQLITE_CONFIG_LOOKASIDE sets the <i>default</i> lookaside size.
** The [SQLITE_DBCONFIG_LOOKASIDE] option to [sqlite3_db_config()] can
** be used to change the lookaside configuration on individual connections.)^
** The [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to change the
** default lookaside configuration at compile-time.
** </dd>
**
** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt>
** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is
@@ -2232,31 +2242,50 @@ struct sqlite3_mem_methods {
** [[SQLITE_DBCONFIG_LOOKASIDE]]
** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt>
** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the
** configuration of the lookaside memory allocator within a database
** configuration of the [lookaside memory allocator] within a database
** connection.
** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i>
** in the [DBCONFIG arguments|usual format].
** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two,
** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE
** should have a total of five parameters.
** ^The first argument (the third parameter to [sqlite3_db_config()] is a
** <ol>
** <li><p>The first argument ("buf") is a
** pointer to a memory buffer to use for lookaside memory.
** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb
** may be NULL in which case SQLite will allocate the
** lookaside buffer itself using [sqlite3_malloc()]. ^The second argument is the
** size of each lookaside buffer slot. ^The third argument is the number of
** slots. The size of the buffer in the first argument must be greater than
** or equal to the product of the second and third arguments. The buffer
** must be aligned to an 8-byte boundary. ^If the second argument to
** SQLITE_DBCONFIG_LOOKASIDE is not a multiple of 8, it is internally
** rounded down to the next smaller multiple of 8. ^(The lookaside memory
** The first argument may be NULL in which case SQLite will allocate the
** lookaside buffer itself using [sqlite3_malloc()].
** <li><P>The second argument ("sz") is the
** size of each lookaside buffer slot. Lookaside is disabled if "sz"
** is less than 8. The "sz" argument should be a multiple of 8 less than
** 65536. If "sz" does not meet this constraint, it is reduced in size until
** it does.
** <li><p>The third argument ("cnt") is the number of slots. Lookaside is disabled
** if "cnt"is less than 1. The "cnt" value will be reduced, if necessary, so
** that the product of "sz" and "cnt" does not exceed 2,147,418,112. The "cnt"
** parameter is usually chosen so that the product of "sz" and "cnt" is less
** than 1,000,000.
** </ol>
** <p>If the "buf" argument is not NULL, then it must
** point to a memory buffer with a size that is greater than
** or equal to the product of "sz" and "cnt".
** The buffer must be aligned to an 8-byte boundary.
** The lookaside memory
** configuration for a database connection can only be changed when that
** connection is not currently using lookaside memory, or in other words
** when the "current value" returned by
** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero.
** when the value returned by [SQLITE_DBSTATUS_LOOKASIDE_USED] is zero.
** Any attempt to change the lookaside memory configuration when lookaside
** memory is in use leaves the configuration unchanged and returns
** [SQLITE_BUSY].)^</dd>
** [SQLITE_BUSY].
** If the "buf" argument is NULL and an attempt
** to allocate memory based on "sz" and "cnt" fails, then
** lookaside is silently disabled.
** <p>
** The [SQLITE_CONFIG_LOOKASIDE] configuration option can be used to set the
** default lookaside configuration at initialization. The
** [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to set the default lookaside
** configuration at compile-time. Typical values for lookaside are 1200 for
** "sz" and 40 to 100 for "cnt".
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_FKEY]]
** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt>
@@ -2993,6 +3022,44 @@ SQLITE_API int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*);
*/
SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms);
/*
** CAPI3REF: Set the Setlk Timeout
** METHOD: sqlite3
**
** This routine is only useful in SQLITE_ENABLE_SETLK_TIMEOUT builds. If
** the VFS supports blocking locks, it sets the timeout in ms used by
** eligible locks taken on wal mode databases by the specified database
** handle. In non-SQLITE_ENABLE_SETLK_TIMEOUT builds, or if the VFS does
** not support blocking locks, this function is a no-op.
**
** Passing 0 to this function disables blocking locks altogether. Passing
** -1 to this function requests that the VFS blocks for a long time -
** indefinitely if possible. The results of passing any other negative value
** are undefined.
**
** Internally, each SQLite database handle store two timeout values - the
** busy-timeout (used for rollback mode databases, or if the VFS does not
** support blocking locks) and the setlk-timeout (used for blocking locks
** on wal-mode databases). The sqlite3_busy_timeout() method sets both
** values, this function sets only the setlk-timeout value. Therefore,
** to configure separate busy-timeout and setlk-timeout values for a single
** database handle, call sqlite3_busy_timeout() followed by this function.
**
** Whenever the number of connections to a wal mode database falls from
** 1 to 0, the last connection takes an exclusive lock on the database,
** then checkpoints and deletes the wal file. While it is doing this, any
** new connection that tries to read from the database fails with an
** SQLITE_BUSY error. Or, if the SQLITE_SETLK_BLOCK_ON_CONNECT flag is
** passed to this API, the new connection blocks until the exclusive lock
** has been released.
*/
SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags);
/*
** CAPI3REF: Flags for sqlite3_setlk_timeout()
*/
#define SQLITE_SETLK_BLOCK_ON_CONNECT 0x01
/*
** CAPI3REF: Convenience Routines For Running Queries
** METHOD: sqlite3
@@ -4012,7 +4079,7 @@ SQLITE_API sqlite3_file *sqlite3_database_file_object(const char*);
**
** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of
** database filename D with corresponding journal file J and WAL file W and
** with N URI parameters key/values pairs in the array P. The result from
** an array P of N URI Key/Value pairs. The result from
** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that
** is safe to pass to routines like:
** <ul>
@@ -4693,7 +4760,7 @@ typedef struct sqlite3_context sqlite3_context;
** METHOD: sqlite3_stmt
**
** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants,
** literals may be replaced by a [parameter] that matches one of following
** literals may be replaced by a [parameter] that matches one of the following
** templates:
**
** <ul>
@@ -4738,7 +4805,7 @@ typedef struct sqlite3_context sqlite3_context;
**
** [[byte-order determination rules]] ^The byte-order of
** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF)
** found in first character, which is removed, or in the absence of a BOM
** found in the first character, which is removed, or in the absence of a BOM
** the byte order is the native byte order of the host
** machine for sqlite3_bind_text16() or the byte order specified in
** the 6th parameter for sqlite3_bind_text64().)^
@@ -4758,7 +4825,7 @@ typedef struct sqlite3_context sqlite3_context;
** or sqlite3_bind_text16() or sqlite3_bind_text64() then
** that parameter must be the byte offset
** where the NUL terminator would occur assuming the string were NUL
** terminated. If any NUL characters occurs at byte offsets less than
** terminated. If any NUL characters occur at byte offsets less than
** the value of the fourth parameter then the resulting string value will
** contain embedded NULs. The result of expressions involving strings
** with embedded NULs is undefined.
@@ -4970,7 +5037,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
** METHOD: sqlite3_stmt
**
** ^These routines provide a means to determine the database, table, and
** table column that is the origin of a particular result column in
** table column that is the origin of a particular result column in a
** [SELECT] statement.
** ^The name of the database or table or column can be returned as
** either a UTF-8 or UTF-16 string. ^The _database_ routines return
@@ -5108,7 +5175,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int);
** other than [SQLITE_ROW] before any subsequent invocation of
** sqlite3_step(). Failure to reset the prepared statement using
** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from
** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1],
** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1]),
** sqlite3_step() began
** calling [sqlite3_reset()] automatically in this circumstance rather
** than returning [SQLITE_MISUSE]. This is not considered a compatibility
@@ -5539,8 +5606,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
**
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
** all application-defined SQL functions that do not need to be
** used inside of triggers, view, CHECK constraints, or other elements of
** the database schema. This flags is especially recommended for SQL
** used inside of triggers, views, CHECK constraints, or other elements of
** the database schema. This flag is especially recommended for SQL
** functions that have side effects or reveal internal application state.
** Without this flag, an attacker might be able to modify the schema of
** a database file to include invocations of the function with parameters
@@ -5571,7 +5638,7 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
** [user-defined window functions|available here].
**
** ^(If the final parameter to sqlite3_create_function_v2() or
** sqlite3_create_window_function() is not NULL, then it is destructor for
** sqlite3_create_window_function() is not NULL, then it is the destructor for
** the application data pointer. The destructor is invoked when the function
** is deleted, either by being overloaded or when the database connection
** closes.)^ ^The destructor is also invoked if the call to
@@ -5971,7 +6038,7 @@ SQLITE_API unsigned int sqlite3_value_subtype(sqlite3_value*);
** METHOD: sqlite3_value
**
** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value]
** object D and returns a pointer to that copy. ^The [sqlite3_value] returned
** object V and returns a pointer to that copy. ^The [sqlite3_value] returned
** is a [protected sqlite3_value] object even if the input is not.
** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
** memory allocation fails. ^If V is a [pointer value], then the result
@@ -6009,7 +6076,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
** allocation error occurs.
**
** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
** determined by the N parameter on first successful call. Changing the
** determined by the N parameter on the first successful call. Changing the
** value of N in any subsequent call to sqlite3_aggregate_context() within
** the same aggregate function instance will not resize the memory
** allocation.)^ Within the xFinal callback, it is customary to set
@@ -6171,7 +6238,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
**
** Security Warning: These interfaces should not be exposed in scripting
** languages or in other circumstances where it might be possible for an
** an attacker to invoke them. Any agent that can invoke these interfaces
** attacker to invoke them. Any agent that can invoke these interfaces
** can probably also take control of the process.
**
** Database connection client data is only available for SQLite
@@ -6285,7 +6352,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** pointed to by the 2nd parameter are taken as the application-defined
** function result. If the 3rd parameter is non-negative, then it
** must be the byte offset into the string where the NUL terminator would
** appear if the string where NUL terminated. If any NUL characters occur
** appear if the string were NUL terminated. If any NUL characters occur
** in the string at a byte offset that is less than the value of the 3rd
** parameter, then the resulting string will contain embedded NULs and the
** result of expressions operating on strings with embedded NULs is undefined.
@@ -6343,7 +6410,7 @@ typedef void (*sqlite3_destructor_type)(void*);
** string and preferably a string literal. The sqlite3_result_pointer()
** routine is part of the [pointer passing interface] added for SQLite 3.20.0.
**
** If these routines are called from within the different thread
** If these routines are called from within a different thread
** than the one containing the application-defined function that received
** the [sqlite3_context] pointer, the results are undefined.
*/
@@ -6749,7 +6816,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
** METHOD: sqlite3
**
** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
** for the N-th database on database connection D, or a NULL pointer of N is
** for the N-th database on database connection D, or a NULL pointer if N is
** out of range. An N value of 0 means the main database file. An N of 1 is
** the "temp" schema. Larger values of N correspond to various ATTACH-ed
** databases.
@@ -6844,7 +6911,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_READ state means that the database is currently
** in a read transaction. Content has been read from the database file
** but nothing in the database file has changed. The transaction state
** will advanced to SQLITE_TXN_WRITE if any changes occur and there are
** will be advanced to SQLITE_TXN_WRITE if any changes occur and there are
** no other conflicting concurrent write transactions. The transaction
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
** [COMMIT].</dd>
@@ -6853,7 +6920,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
** <dd>The SQLITE_TXN_WRITE state means that the database is currently
** in a write transaction. Content has been written to the database file
** but has not yet committed. The transaction state will change to
** to SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
** SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
*/
#define SQLITE_TXN_NONE 0
#define SQLITE_TXN_READ 1
@@ -7004,6 +7071,8 @@ SQLITE_API int sqlite3_autovacuum_pages(
**
** ^The second argument is a pointer to the function to invoke when a
** row is updated, inserted or deleted in a rowid table.
** ^The update hook is disabled by invoking sqlite3_update_hook()
** with a NULL pointer as the second parameter.
** ^The first argument to the callback is a copy of the third argument
** to sqlite3_update_hook().
** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE],
@@ -7132,7 +7201,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** CAPI3REF: Impose A Limit On Heap Size
**
** These interfaces impose limits on the amount of heap memory that will be
** by all database connections within a single process.
** used by all database connections within a single process.
**
** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the
** soft limit on the amount of heap memory that may be allocated by SQLite.
@@ -7190,7 +7259,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
** </ul>)^
**
** The circumstances under which SQLite will enforce the heap limits may
** changes in future releases of SQLite.
** change in future releases of SQLite.
*/
SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N);
SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N);
@@ -7305,8 +7374,8 @@ SQLITE_API int sqlite3_table_column_metadata(
** ^The entry point is zProc.
** ^(zProc may be 0, in which case SQLite will try to come up with an
** entry point name on its own. It first tries "sqlite3_extension_init".
** If that does not work, it constructs a name "sqlite3_X_init" where the
** X is consists of the lower-case equivalent of all ASCII alphabetic
** If that does not work, it constructs a name "sqlite3_X_init" where
** X consists of the lower-case equivalent of all ASCII alphabetic
** characters in the filename from the last "/" to the first following
** "." and omitting any initial "lib".)^
** ^The sqlite3_load_extension() interface returns
@@ -7377,7 +7446,7 @@ SQLITE_API int sqlite3_enable_load_extension(sqlite3 *db, int onoff);
** ^(Even though the function prototype shows that xEntryPoint() takes
** no arguments and returns void, SQLite invokes xEntryPoint() with three
** arguments and expects an integer result as if the signature of the
** entry point where as follows:
** entry point were as follows:
**
** <blockquote><pre>
** &nbsp; int xEntryPoint(
@@ -7541,7 +7610,7 @@ struct sqlite3_module {
** virtual table and might not be checked again by the byte code.)^ ^(The
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
** is left in its default setting of false, the constraint will always be
** checked separately in byte code. If the omit flag is change to true, then
** checked separately in byte code. If the omit flag is changed to true, then
** the constraint may or may not be checked in byte code. In other words,
** when the omit flag is true there is no guarantee that the constraint will
** not be checked again using byte code.)^
@@ -7567,7 +7636,7 @@ struct sqlite3_module {
** The xBestIndex method may optionally populate the idxFlags field with a
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
** output to show the idxNum has hex instead of as decimal. Another flag is
** output to show the idxNum as hex instead of as decimal. Another flag is
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
** return at most one row.
**
@@ -7708,7 +7777,7 @@ struct sqlite3_index_info {
** the implementation of the [virtual table module]. ^The fourth
** parameter is an arbitrary client data pointer that is passed through
** into the [xCreate] and [xConnect] methods of the virtual table module
** when a new virtual table is be being created or reinitialized.
** when a new virtual table is being created or reinitialized.
**
** ^The sqlite3_create_module_v2() interface has a fifth parameter which
** is a pointer to a destructor for the pClientData. ^SQLite will
@@ -7873,7 +7942,7 @@ typedef struct sqlite3_blob sqlite3_blob;
** in *ppBlob. Otherwise an [error code] is returned and, unless the error
** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided
** the API is not misused, it is always safe to call [sqlite3_blob_close()]
** on *ppBlob after this function it returns.
** on *ppBlob after this function returns.
**
** This function fails with SQLITE_ERROR if any of the following are true:
** <ul>
@@ -7993,7 +8062,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
**
** ^Returns the size in bytes of the BLOB accessible via the
** successfully opened [BLOB handle] in its only argument. ^The
** incremental blob I/O routines can only read or overwriting existing
** incremental blob I/O routines can only read or overwrite existing
** blob content; they cannot change the size of a blob.
**
** This routine only works on a [BLOB handle] which has been created
@@ -8143,7 +8212,7 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
** ^The sqlite3_mutex_alloc() routine allocates a new
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
** routine returns NULL if it is unable to allocate the requested
** mutex. The argument to sqlite3_mutex_alloc() must one of these
** mutex. The argument to sqlite3_mutex_alloc() must be one of these
** integer constants:
**
** <ul>
@@ -8376,7 +8445,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
** CAPI3REF: Retrieve the mutex for a database connection
** METHOD: sqlite3
**
** ^This interface returns a pointer the [sqlite3_mutex] object that
** ^This interface returns a pointer to the [sqlite3_mutex] object that
** serializes access to the [database connection] given in the argument
** when the [threading mode] is Serialized.
** ^If the [threading mode] is Single-thread or Multi-thread then this
@@ -8499,7 +8568,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** CAPI3REF: SQL Keyword Checking
**
** These routines provide access to the set of SQL language keywords
** recognized by SQLite. Applications can uses these routines to determine
** recognized by SQLite. Applications can use these routines to determine
** whether or not a specific identifier needs to be escaped (for example,
** by enclosing in double-quotes) so as not to confuse the parser.
**
@@ -8667,7 +8736,7 @@ SQLITE_API void sqlite3_str_reset(sqlite3_str*);
** content of the dynamic string under construction in X. The value
** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X
** and might be freed or altered by any subsequent method on the same
** [sqlite3_str] object. Applications must not used the pointer returned
** [sqlite3_str] object. Applications must not use the pointer returned by
** [sqlite3_str_value(X)] after any subsequent method call on the same
** object. ^Applications may change the content of the string returned
** by [sqlite3_str_value(X)] as long as they do not write into any bytes
@@ -8753,7 +8822,7 @@ SQLITE_API int sqlite3_status64(
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
** buffer and where forced to overflow to [sqlite3_malloc()]. The
** returned value includes allocations that overflowed because they
** where too large (they were larger than the "sz" parameter to
** were too large (they were larger than the "sz" parameter to
** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because
** no space was left in the page cache.</dd>)^
**
@@ -8837,28 +8906,29 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt>
** <dd>This parameter returns the number of malloc attempts that were
** satisfied using lookaside memory. Only the high-water value is meaningful;
** the current value is always zero.)^
** the current value is always zero.</dd>)^
**
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt>
** <dd>This parameter returns the number malloc attempts that might have
** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to the amount of
** memory requested being larger than the lookaside slot size.
** Only the high-water value is meaningful;
** the current value is always zero.)^
** the current value is always zero.</dd>)^
**
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]]
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt>
** <dd>This parameter returns the number malloc attempts that might have
** <dd>This parameter returns the number of malloc attempts that might have
** been satisfied using lookaside memory but failed due to all lookaside
** memory already being in use.
** Only the high-water value is meaningful;
** the current value is always zero.)^
** the current value is always zero.</dd>)^
**
** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap
** memory used by all pager caches associated with the database connection.)^
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt>
@@ -8867,10 +8937,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** memory used by that pager cache is divided evenly between the attached
** connections.)^ In other words, if none of the pager caches associated
** with the database connection are shared, this request returns the same
** value as DBSTATUS_CACHE_USED. Or, if one or more or the pager caches are
** value as DBSTATUS_CACHE_USED. Or, if one or more of the pager caches are
** shared, the value returned by this call will be smaller than that returned
** by DBSTATUS_CACHE_USED. ^The highwater mark associated with
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.</dd>
**
** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap
@@ -8880,6 +8950,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** schema memory is shared with other database connections due to
** [shared cache mode] being enabled.
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
** </dd>
**
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
** <dd>This parameter returns the approximate number of bytes of heap
@@ -8916,7 +8987,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** been written to disk in the middle of a transaction due to the page
** cache overflowing. Transactions are more efficient if they are written
** to disk all at once. When pages spill mid-transaction, that introduces
** additional overhead. This parameter can be used help identify
** additional overhead. This parameter can be used to help identify
** inefficiencies that can be resolved by increasing the cache size.
** </dd>
**
@@ -8987,13 +9058,13 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt>
** <dd>^This is the number of sort operations that have occurred.
** A non-zero value in this counter may indicate an opportunity to
** improvement performance through careful use of indices.</dd>
** improve performance through careful use of indices.</dd>
**
** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt>
** <dd>^This is the number of rows inserted into transient indices that
** were created automatically in order to help joins run faster.
** A non-zero value in this counter may indicate an opportunity to
** improvement performance by adding permanent indices that do not
** improve performance by adding permanent indices that do not
** need to be reinitialized each time the statement is run.</dd>
**
** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt>
@@ -9002,19 +9073,19 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** to 2147483647. The number of virtual machine operations can be
** used as a proxy for the total work done by the prepared statement.
** If the number of virtual machine operations exceeds 2147483647
** then the value returned by this statement status code is undefined.
** then the value returned by this statement status code is undefined.</dd>
**
** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt>
** <dd>^This is the number of times that the prepare statement has been
** automatically regenerated due to schema changes or changes to
** [bound parameters] that might affect the query plan.
** [bound parameters] that might affect the query plan.</dd>
**
** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt>
** <dd>^This is the number of times that the prepared statement has
** been run. A single "run" for the purposes of this counter is one
** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()].
** The counter is incremented on the first [sqlite3_step()] call of each
** cycle.
** cycle.</dd>
**
** [[SQLITE_STMTSTATUS_FILTER_MISS]]
** [[SQLITE_STMTSTATUS_FILTER HIT]]
@@ -9024,7 +9095,7 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** step was bypassed because a Bloom filter returned not-found. The
** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of
** times that the Bloom filter returned a find, and thus the join step
** had to be processed as normal.
** had to be processed as normal.</dd>
**
** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt>
** <dd>^This is the approximate number of bytes of heap memory
@@ -9129,9 +9200,9 @@ struct sqlite3_pcache_page {
** SQLite will typically create one cache instance for each open database file,
** though this is not guaranteed. ^The
** first parameter, szPage, is the size in bytes of the pages that must
** be allocated by the cache. ^szPage will always a power of two. ^The
** be allocated by the cache. ^szPage will always be a power of two. ^The
** second parameter szExtra is a number of bytes of extra storage
** associated with each page cache entry. ^The szExtra parameter will
** associated with each page cache entry. ^The szExtra parameter will be
** a number less than 250. SQLite will use the
** extra szExtra bytes on each page to store metadata about the underlying
** database page on disk. The value passed into szExtra depends
@@ -9139,17 +9210,17 @@ struct sqlite3_pcache_page {
** ^The third argument to xCreate(), bPurgeable, is true if the cache being
** created will be used to cache database pages of a file stored on disk, or
** false if it is used for an in-memory database. The cache implementation
** does not have to do anything special based with the value of bPurgeable;
** does not have to do anything special based upon the value of bPurgeable;
** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will
** never invoke xUnpin() except to deliberately delete a page.
** ^In other words, calls to xUnpin() on a cache with bPurgeable set to
** false will always have the "discard" flag set to true.
** ^Hence, a cache created with bPurgeable false will
** ^Hence, a cache created with bPurgeable set to false will
** never contain any unpinned pages.
**
** [[the xCachesize() page cache method]]
** ^(The xCachesize() method may be called at any time by SQLite to set the
** suggested maximum cache-size (number of pages stored by) the cache
** suggested maximum cache-size (number of pages stored) for the cache
** instance passed as the first argument. This is the value configured using
** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable
** parameter, the implementation is not required to do anything with this
@@ -9176,12 +9247,12 @@ struct sqlite3_pcache_page {
** implementation must return a pointer to the page buffer with its content
** intact. If the requested page is not already in the cache, then the
** cache implementation should use the value of the createFlag
** parameter to help it determined what action to take:
** parameter to help it determine what action to take:
**
** <table border=1 width=85% align=center>
** <tr><th> createFlag <th> Behavior when page is not already in cache
** <tr><td> 0 <td> Do not allocate a new page. Return NULL.
** <tr><td> 1 <td> Allocate a new page if it easy and convenient to do so.
** <tr><td> 1 <td> Allocate a new page if it is easy and convenient to do so.
** Otherwise return NULL.
** <tr><td> 2 <td> Make every effort to allocate a new page. Only return
** NULL if allocating a new page is effectively impossible.
@@ -9198,7 +9269,7 @@ struct sqlite3_pcache_page {
** as its second argument. If the third parameter, discard, is non-zero,
** then the page must be evicted from the cache.
** ^If the discard parameter is
** zero, then the page may be discarded or retained at the discretion of
** zero, then the page may be discarded or retained at the discretion of the
** page cache implementation. ^The page cache implementation
** may choose to evict unpinned pages at any time.
**
@@ -9216,7 +9287,7 @@ struct sqlite3_pcache_page {
** When SQLite calls the xTruncate() method, the cache must discard all
** existing cache entries with page numbers (keys) greater than or equal
** to the value of the iLimit parameter passed to xTruncate(). If any
** of these pages are pinned, they are implicitly unpinned, meaning that
** of these pages are pinned, they become implicitly unpinned, meaning that
** they can be safely discarded.
**
** [[the xDestroy() page cache method]]
@@ -9396,7 +9467,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** external process or via a database connection other than the one being
** used by the backup operation, then the backup will be automatically
** restarted by the next call to sqlite3_backup_step(). ^If the source
** database is modified by the using the same database connection as is used
** database is modified by using the same database connection as is used
** by the backup operation, then the backup database is automatically
** updated at the same time.
**
@@ -9413,7 +9484,7 @@ typedef struct sqlite3_backup sqlite3_backup;
** and may not be used following a call to sqlite3_backup_finish().
**
** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no
** sqlite3_backup_step() errors occurred, regardless or whether or not
** sqlite3_backup_step() errors occurred, regardless of whether or not
** sqlite3_backup_step() completed.
** ^If an out-of-memory condition or IO error occurred during any prior
** sqlite3_backup_step() call on the same [sqlite3_backup] object, then
@@ -9515,7 +9586,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** application receives an SQLITE_LOCKED error, it may call the
** sqlite3_unlock_notify() method with the blocked connection handle as
** the first argument to register for a callback that will be invoked
** when the blocking connections current transaction is concluded. ^The
** when the blocking connection's current transaction is concluded. ^The
** callback is invoked from within the [sqlite3_step] or [sqlite3_close]
** call that concludes the blocking connection's transaction.
**
@@ -9535,7 +9606,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** blocked connection already has a registered unlock-notify callback,
** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is
** called with a NULL pointer as its second argument, then any existing
** unlock-notify callback is canceled. ^The blocked connections
** unlock-notify callback is canceled. ^The blocked connection's
** unlock-notify callback may also be canceled by closing the blocked
** connection using [sqlite3_close()].
**
@@ -9933,7 +10004,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** support constraints. In this configuration (which is the default) if
** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire
** statement is rolled back as if [ON CONFLICT | OR ABORT] had been
** specified as part of the users SQL statement, regardless of the actual
** specified as part of the user's SQL statement, regardless of the actual
** ON CONFLICT mode specified.
**
** If X is non-zero, then the virtual table implementation guarantees
@@ -9967,7 +10038,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt>
** <dd>Calls of the form
** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the
** the [xConnect] or [xCreate] methods of a [virtual table] implementation
** [xConnect] or [xCreate] methods of a [virtual table] implementation
** identify that virtual table as being safe to use from within triggers
** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the
** virtual table can do no serious harm even if it is controlled by a
@@ -10135,7 +10206,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** </table>
**
** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered
** values are the same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS"
** (or "IS NOT DISTINCT FROM") and not "==".
**
@@ -10145,7 +10216,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
**
** ^A virtual table implementation is always free to return rows in any order
** it wants, as long as the "orderByConsumed" flag is not set. ^When the
** the "orderByConsumed" flag is unset, the query planner will add extra
** "orderByConsumed" flag is unset, the query planner will add extra
** [bytecode] to ensure that the final results returned by the SQL query are
** ordered correctly. The use of the "orderByConsumed" flag and the
** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful
@@ -10242,7 +10313,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
** xFilter method which invokes these routines, and specifically
** a parameter that was previously selected for all-at-once IN constraint
** processing use the [sqlite3_vtab_in()] interface in the
** processing using the [sqlite3_vtab_in()] interface in the
** [xBestIndex|xBestIndex method]. ^(If the X parameter is not
** an xFilter argument that was selected for all-at-once IN constraint
** processing, then these routines return [SQLITE_ERROR].)^
@@ -10297,7 +10368,7 @@ SQLITE_API int sqlite3_vtab_in_next(sqlite3_value *pVal, sqlite3_value **ppOut);
** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V)
** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th
** constraint is not available. ^The sqlite3_vtab_rhs_value() interface
** can return an result code other than SQLITE_OK or SQLITE_NOTFOUND if
** can return a result code other than SQLITE_OK or SQLITE_NOTFOUND if
** something goes wrong.
**
** The sqlite3_vtab_rhs_value() interface is usually only successful if
@@ -10325,8 +10396,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** KEYWORDS: {conflict resolution mode}
**
** These constants are returned by [sqlite3_vtab_on_conflict()] to
** inform a [virtual table] implementation what the [ON CONFLICT] mode
** is for the SQL statement being evaluated.
** inform a [virtual table] implementation of the [ON CONFLICT] mode
** for the SQL statement being evaluated.
**
** Note that the [SQLITE_IGNORE] constant is also used as a potential
** return value from the [sqlite3_set_authorizer()] callback and that
@@ -10366,39 +10437,39 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt>
** <dd>^The "double" variable pointed to by the V parameter will be set to the
** query planner's estimate for the average number of rows output from each
** iteration of the X-th loop. If the query planner's estimates was accurate,
** iteration of the X-th loop. If the query planner's estimate was accurate,
** then this value will approximate the quotient NVISIT/NLOOP and the
** product of this value for all prior loops with the same SELECTID will
** be the NLOOP value for the current loop.
** be the NLOOP value for the current loop.</dd>
**
** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the name of the index or table
** used for the X-th loop.
** used for the X-th loop.</dd>
**
** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
** description for the X-th loop.
** description for the X-th loop.</dd>
**
** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
** <dd>^The "int" variable pointed to by the V parameter will be set to the
** id for the X-th query plan element. The id value is unique within the
** statement. The select-id is the same value as is output in the first
** column of an [EXPLAIN QUERY PLAN] query.
** column of an [EXPLAIN QUERY PLAN] query.</dd>
**
** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
** <dd>The "int" variable pointed to by the V parameter will be set to the
** the id of the parent of the current query element, if applicable, or
** id of the parent of the current query element, if applicable, or
** to zero if the query element has no parent. This is the same value as
** returned in the second column of an [EXPLAIN QUERY PLAN] query.
** returned in the second column of an [EXPLAIN QUERY PLAN] query.</dd>
**
** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
** <dd>The sqlite3_int64 output value is set to the number of cycles,
** according to the processor time-stamp counter, that elapsed while the
** query element was being processed. This value is not available for
** all query elements - if it is unavailable the output variable is
** set to -1.
** set to -1.</dd>
** </dl>
*/
#define SQLITE_SCANSTAT_NLOOP 0
@@ -10439,8 +10510,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
**
** Parameter "idx" identifies the specific query element to retrieve statistics
** for. Query elements are numbered starting from zero. A value of -1 may be
** to query for statistics regarding the entire query. ^If idx is out of range
** for. Query elements are numbered starting from zero. A value of -1 may
** retrieve statistics for the entire query. ^If idx is out of range
** - less than -1 or greater than or equal to the total number of query
** elements used to implement the statement - a non-zero value is returned and
** the variable that pOut points to is unchanged.
@@ -10483,7 +10554,7 @@ SQLITE_API void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*);
** METHOD: sqlite3
**
** ^If a write-transaction is open on [database connection] D when the
** [sqlite3_db_cacheflush(D)] interface invoked, any dirty
** [sqlite3_db_cacheflush(D)] interface is invoked, any dirty
** pages in the pager-cache that are not currently in use are written out
** to disk. A dirty page may be in use if a database cursor created by an
** active SQL statement is reading from it, or if it is page 1 of a database
@@ -10597,8 +10668,8 @@ SQLITE_API int sqlite3_db_cacheflush(sqlite3*);
** triggers; and so forth.
**
** When the [sqlite3_blob_write()] API is used to update a blob column,
** the pre-update hook is invoked with SQLITE_DELETE. This is because the
** in this case the new values are not available. In this case, when a
** the pre-update hook is invoked with SQLITE_DELETE, because
** the new values are not yet available. In this case, when a
** callback made with op==SQLITE_DELETE is actually a write using the
** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns
** the index of the column being written. In other cases, where the
@@ -10851,7 +10922,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** For an ordinary on-disk database file, the serialization is just a
** copy of the disk file. For an in-memory database or a "TEMP" database,
** the serialization is the same sequence of bytes which would be written
** to disk if that database where backed up to disk.
** to disk if that database were backed up to disk.
**
** The usual case is that sqlite3_serialize() copies the serialization of
** the database into memory obtained from [sqlite3_malloc64()] and returns
@@ -10860,7 +10931,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations
** are made, and the sqlite3_serialize() function will return a pointer
** to the contiguous memory representation of the database that SQLite
** is currently using for that database, or NULL if the no such contiguous
** is currently using for that database, or NULL if no such contiguous
** memory representation of the database exists. A contiguous memory
** representation of the database will usually only exist if there has
** been a prior call to [sqlite3_deserialize(D,S,...)] with the same
@@ -10931,7 +11002,7 @@ SQLITE_API unsigned char *sqlite3_serialize(
** database is currently in a read transaction or is involved in a backup
** operation.
**
** It is not possible to deserialized into the TEMP database. If the
** It is not possible to deserialize into the TEMP database. If the
** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the
** function returns SQLITE_ERROR.
**
@@ -10953,7 +11024,7 @@ SQLITE_API int sqlite3_deserialize(
sqlite3 *db, /* The database connection */
const char *zSchema, /* Which DB to reopen with the deserialization */
unsigned char *pData, /* The serialized database content */
sqlite3_int64 szDb, /* Number bytes in the deserialization */
sqlite3_int64 szDb, /* Number of bytes in the deserialization */
sqlite3_int64 szBuf, /* Total size of buffer pData[] */
unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */
);
@@ -10961,7 +11032,7 @@ SQLITE_API int sqlite3_deserialize(
/*
** CAPI3REF: Flags for sqlite3_deserialize()
**
** The following are allowed values for 6th argument (the F argument) to
** The following are allowed values for the 6th argument (the F argument) to
** the [sqlite3_deserialize(D,S,P,N,M,F)] interface.
**
** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization
@@ -11486,9 +11557,10 @@ SQLITE_API void sqlite3session_table_filter(
** is inserted while a session object is enabled, then later deleted while
** the same session object is disabled, no INSERT record will appear in the
** changeset, even though the delete took place while the session was disabled.
** Or, if one field of a row is updated while a session is disabled, and
** another field of the same row is updated while the session is enabled, the
** resulting changeset will contain an UPDATE change that updates both fields.
** Or, if one field of a row is updated while a session is enabled, and
** then another field of the same row is updated while the session is disabled,
** the resulting changeset will contain an UPDATE change that updates both
** fields.
*/
SQLITE_API int sqlite3session_changeset(
sqlite3_session *pSession, /* Session object */
@@ -11560,8 +11632,9 @@ SQLITE_API sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession
** database zFrom the contents of the two compatible tables would be
** identical.
**
** It an error if database zFrom does not exist or does not contain the
** required compatible table.
** Unless the call to this function is a no-op as described above, it is an
** error if database zFrom does not exist or does not contain the required
** compatible table.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
@@ -11696,7 +11769,7 @@ SQLITE_API int sqlite3changeset_start_v2(
** The following flags may passed via the 4th parameter to
** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]:
**
** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd>
** <dt>SQLITE_CHANGESETSTART_INVERT <dd>
** Invert the changeset while iterating through it. This is equivalent to
** inverting a changeset using sqlite3changeset_invert() before applying it.
** It is an error to specify this flag with a patchset.
@@ -12011,19 +12084,6 @@ SQLITE_API int sqlite3changeset_concat(
void **ppOut /* OUT: Buffer containing output changeset */
);
/*
** CAPI3REF: Upgrade the Schema of a Changeset/Patchset
*/
SQLITE_API int sqlite3changeset_upgrade(
sqlite3 *db,
const char *zDb,
int nIn, const void *pIn, /* Input changeset */
int *pnOut, void **ppOut /* OUT: Inverse of input */
);
/*
** CAPI3REF: Changegroup Handle
**

View File

@@ -366,6 +366,8 @@ struct sqlite3_api_routines {
/* Version 3.44.0 and later */
void *(*get_clientdata)(sqlite3*,const char*);
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
/* Version 3.50.0 and later */
int (*setlk_timeout)(sqlite3*,int,int);
};
/*
@@ -699,6 +701,8 @@ typedef int (*sqlite3_loadext_entry)(
/* Version 3.44.0 and later */
#define sqlite3_get_clientdata sqlite3_api->get_clientdata
#define sqlite3_set_clientdata sqlite3_api->set_clientdata
/* Version 3.50.0 and later */
#define sqlite3_setlk_timeout sqlite3_api->setlk_timeout
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)

View File

@@ -0,0 +1,40 @@
# Connecting with Manyverse
Communication with [Manyverse](https://www.manyver.se/) should Just Work (tm).
This document is intended as a cheat sheet for the instances where it doesn't.
If your experience differs, please share so we can make things better.
## Connecting Manyverse to the tildefriends.net room
Open the `Connections` tab. This is from the desktop app, but mobile is similar.
![Manyverse connections tab](manyverse_connections_tab.png)
Open the `Connections Panel`.
![Manyverse connections panel](manyverse_connections_panel.png)
Use the `Add Connection` button at the bottom right to open the dialog to enter
a connections string to add a new connection.
![Manyverse connections panel](manyverse_paste_invite_code.png)
Copy the tildefriends.net room code from https://www.tildefriends.net/~cory/room/.
![Tilde Friends room code](tildefriends_room_app.png)
Paste.
On mobile especially, make sure the full text is pasted without modification.
![Manyverse invite code](manyverse_code.png)
Click `Done`, and you should be connected successfully. tildefriends.net is
all things: a room, a pub, and a client, so you should be able to start replicating
immediately as well as find other similarly connected people with whom to establish
further connections.
When logged into tildefriends.net, active connections it sees can be found on
the `Connections` tab: https://www.tildefriends.net/~core/ssb/#connections,
which may indicate errors if you find yourself disconnecting.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -14,7 +14,7 @@
- upload to Apple with dist-ios on macos
- nix
- june and december: update release version
- run `nix flake update`
- run `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
- comment out the hash in default.nix
- update the version
- run `nix-build`

289
docs/usage.md Normal file
View File

@@ -0,0 +1,289 @@
# CLI Usage
## tildefriends -h
```
Usage: out/debug/tildefriends command [command-options]
commands:
run - Run tildefriends (default).
sandbox - Run a sandboxed tildefriends sandbox process (used internally).
import - Import apps from file to the database.
export - Export apps from the database to file.
publish - Append a message to a feed.
private - Append a private post message to a feed.
create_invite - Create an invite.
get_sequence - Get the last sequence number for a feed.
get_identity - Get the server account identity.
get_profile - Get profile information for the given identity.
get_contacts - Get information about followed, blocked, and friend identities.
has_blob - Check whether a blob is in the blob store.
get_blob - Read a file from the blob store.
store_blob - Write a file to the blob store.
verify - Verify a feed.
test - Test SSB.
```
## tildefriends run -h
```
Usage: out/debug/tildefriends run [options]
Run tildefriends (default).
options:
-s, --script script Script to run (default: core/core.js).
-d, --db-path path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-k, --ssb-network-key key SSB network key to use.
-n, --count count Number of instances to run.
-a, --args args Arguments of the format key=value,foo=bar,verbose=true (note: these are persisted to the database).
code_of_conduct (default: ""): Code of conduct presented at sign-in.
ssb_port (default: 8008): Port on which to listen for SSB secure handshake connections.
http_local_only (default: false): Whether to bind http(s) to the loopback address. Otherwise any.
http_port (default: 12345): Port on which to listen for HTTP connections.
https_port (default: 0): Port on which to listen for secure HTTP connections.
out_http_port_file (default: ""): File to which to write bound HTTP port.
blob_fetch_age_seconds (default: -1): Only blobs mentioned more recently than this age will be automatically fetched.
blob_expire_age_seconds (default: -1): Blobs older than this will be automatically deleted.
fetch_hosts (default: ""): Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.
http_redirect (default: ""): If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "http://example.com")
index (default: "/~core/intro/"): Default path.
index_map (default: ""): Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"
peer_exchange (default: false): Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.
replicator (default: true): Enable message and blob replication.
room (default: true): Enable peers to tunnel through this instance as a room.
room_name (default: "tilde friends tunnel"): Name of the room.
seeds_host (default: "seeds.tildefriends.net"): Hostname for seed connections.
account_registration (default: true): Allow registration of new accounts.
replication_hops (default: 2): Number of hops to replicate (1 = direct follows, 2 = follows of follows, etc.).
delete_stale_feeds (default: false): Periodically delete feeds that aren't visible from local accounts or related follows.
talk_to_strangers (default: true): Whether connections are accepted from accounts that aren't in the replication range or otherwise already known.
autologin (default: false): Whether mobile autologin is supported.
broadcast (default: true): Send network discovery broadcasts.
discovery (default: true): Receive network discovery broadcasts.
stay_connected (default: false): Whether to attempt to keep several peer connections open.
-o, --one-proc Run everything in one process (unsafely!).
-z, --zip path Zip archive from which to load files.
-v, --verbose Log raw messages.
-h, --help Show this usage information.
```
## tildefriends sandbox -h
```
Usage: out/debug/tildefriends sandbox [options]
Run a sandboxed tildefriends sandbox process (used internally).
options:
-h, --help Show this usage information.
-f, --fd File descriptor with which to communicate with parent process.
```
## tildefriends import -h
```
Usage: out/debug/tildefriends import [options] [paths...]
Import apps from file to the database.
options:
-u, --user user User into whose account apps will be imported (default: "import").
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-h, --help Show this usage information.
```
## tildefriends export -h
```
Usage: out/debug/tildefriends export [options] [paths...]
Export apps from the database to file.
options:
-u, --user user User from whose account apps will be exported (default: "core").
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-h, --help Show this usage information.
paths Paths of apps to export (example: /~core/ssb /~user/app).
```
## tildefriends publish -h
```
Usage: out/debug/tildefriends publish [options]
Append a message to a feed.
options:
-u, --user user User owning identity with which to publish.
-i, --id identity Identity with which to publish message.
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-c, --content json JSON content of message to publish.
-h, --help Show this usage information.
```
## tildefriends private -h
```
Usage: out/debug/tildefriends private [options]
Append a private post message to a feed.
options:
-u, --user user User owning identity with which to publish (optional).
-i, --id identity Identity with which to publish message.
-r, --recipients recipients Recipient identities.
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-t, --text text Private post text.
-h, --help Show this usage information.
```
## tildefriends create_invite -h
```
Usage: out/debug/tildefriends create_invite [options]
Create an invite.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-i, --identity identity Account from which to get latest sequence number.
-a, --address address Address to which the recipient will connect.
-p, --port port Port to which the recipient will connect.
-u, --use_count count Number of times this invite may be used (default: 1).
-e, --expires seconds How long this invite is valid in seconds (-1 for indefinitely, default: 1 hour).
-h, --help Show this usage information.
```
## tildefriends get_sequence -h
```
Usage: out/debug/tildefriends get_sequence [options]
Get the last sequence number for a feed.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-i, --identity identity Account from which to get latest sequence number.
-h, --help Show this usage information.
```
## tildefriends get_identity -h
```
Usage: out/debug/tildefriends get_identity [options]
Get the server account identity.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-h, --help Show this usage information.
```
## tildefriends get_profile -h
```
Usage: out/debug/tildefriends get_profile [options]
Get profile information for the given identity.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-i, --identity identity Account for which to get profile information.
-h, --help Show this usage information.
```
## tildefriends get_contacts -h
```
Usage: out/debug/tildefriends get_contacts [options]
Get information about followed, blocked, and friend identities.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-i, --identity identity Account from which to get contact information.
-h, --help Show this usage information.
```
## tildefriends has_blob -h
```
Usage: out/debug/tildefriends has_blob [options]
Check whether a blob is in the blob store.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-b, --blob_id blob_id ID of blob to query.
-h, --help Show this usage information.
```
## tildefriends get_blob -h
```
Usage: out/debug/tildefriends get_blob [options]
Read a file from the blob store.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-b, --blob blob_id Blob identifier to retrieve.
-o, --output file_path Location to write the retrieved blob.
-h, --help Show this usage information.
```
## tildefriends store_blob -h
```
Usage: out/debug/tildefriends store_blob [options]
Write a file to the blob store.
options:
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-f, --file file_path Path to file to add to the blob store.
-h, --help Show this usage information.
```
## tildefriends verify -h
```
Usage: out/debug/tildefriends verify [options]
Verify a feed.
options:
-i, --identity identity Identity to verify.
-s, --sequence sequence Sequence number to debug.
-d, --db-path db_path SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
-h, --help Show this usage information.
```
## tildefriends test -h
```
Usage: out/debug/tildefriends test [options]
Test SSB.
options:
-t, --tests tests Comma-separated list of tests to run. (default: all)
-h, --help Show this usage information.
```

8
flake.lock generated
View File

@@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1745279238,
"narHash": "sha256-AQ7M9wTa/Pa/kK5pcGTgX/DGqMHyzsyINfN7ktsI7Fo=",
"lastModified": 1753749649,
"narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9684b53175fc6c09581e94cc85f05ab77464c7e3",
"rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -2,7 +2,7 @@
description = "Tilde Friends is a platform for making, running, and sharing web applications.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};

View File

@@ -0,0 +1,14 @@
* Improve load times.
* Fix the messages_refs table, and make it usable for hashtags.
* Fix a circumstance where we would fail to call promise callbacks.
* Fix the private messages tab.
* Fix the room app.
* Expose followed accounts in a user's profile.
* Show connection status in the sidebar.
* Simplify placeholder messages.
* Only show "Mark as Read" when relevant.
* Treat profile images more like post images.
* Limit the WAL file size.
* Updates:
* CodeMirror
* sqlite 3.50.1

View File

@@ -0,0 +1,2 @@
* Updating Android SDK+target versions.
* Minor UI improvements.

View File

@@ -0,0 +1,14 @@
* Added an option to stay connected to a handful of peers.
* Load more messages at a time.
* Fix a set of Android not responding errors.
* Target Android 15 (API level 35) to meet new requirements.
* Support JS-less webapps.
* Fix unnecessary tunnel disconnects.
* Many small user interface tweaks.
* Update:
* CodeMirror
* OpenSSL 3.5.1
* lit 3.3.1
* picohttpparser
* speedscope 1.23.0
* sqlite 3.50.4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 108 KiB

7
package-lock.json generated
View File

@@ -11,9 +11,10 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},

View File

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

View File

@@ -50,6 +50,7 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsWebView web_view;
String base_url;
String port_file_path;
Thread create_thread;
Thread server_thread;
ServiceConnection service_connection;
FileObserver observer;
@@ -67,17 +68,7 @@ public class TildeFriendsActivity extends Activity {
public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path, ConnectivityManager connectivity_manager);
public static native int tf_sandbox_main(int pipe_fd);
@Override
protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.penaltyLog()
.build());
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
private void createThread() {
web_view = (TildeFriendsWebView)findViewById(R.id.web);
set_status("Extracting executable...");
Log.w("tildefriends", String.format("getFilesDir() is %s", getFilesDir().toString()));
@@ -90,14 +81,14 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsActivity activity = this;
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
set_status("Starting server...");
server_thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
Log.w("tildefriends", "Calling tf_server_main.");
int result = tf_server_main(
getFilesDir().toString(),
@@ -109,152 +100,184 @@ public class TildeFriendsActivity extends Activity {
});
server_thread.start();
web_view.getSettings().setJavaScriptEnabled(true);
web_view.getSettings().setDatabaseEnabled(true);
web_view.getSettings().setDomStorageEnabled(true);
runOnUiThread(() -> {
web_view.getSettings().setJavaScriptEnabled(true);
web_view.getSettings().setDomStorageEnabled(true);
set_database_path();
set_database_enabled();
set_database_path();
web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) {
Log.w("tildefriends", "Let's download: " + url + " (" + content_disposition + ")");
String file_name = URLUtil.guessFileName(url, content_disposition, mime_type);
if (url.startsWith("data:") && url.indexOf(',') != -1) {
String b64 = url.substring(url.indexOf(',') + 1);
byte[] data = Base64.decode(b64, Base64.DEFAULT);
Log.w("tildefriends", "Downloaded " + data.length + " bytes.");
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
try (OutputStream stream = new FileOutputStream(new File(path, file_name))) {
stream.write(data);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e.toString());
web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) {
Log.w("tildefriends", "Let's download: " + url + " (" + content_disposition + ")");
String file_name = URLUtil.guessFileName(url, content_disposition, mime_type);
if (url.startsWith("data:") && url.indexOf(',') != -1) {
String b64 = url.substring(url.indexOf(',') + 1);
byte[] data = Base64.decode(b64, Base64.DEFAULT);
Log.w("tildefriends", "Downloaded " + data.length + " bytes.");
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
try (OutputStream stream = new FileOutputStream(new File(path, file_name))) {
stream.write(data);
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e.toString());
}
Toast.makeText(getApplicationContext(), "Downloaded File", Toast.LENGTH_LONG).show();
} else {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setMimeType(mime_type);
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Downloading file...");
request.setTitle(file_name);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, URLUtil.guessFileName(url, content_disposition, mime_type));
DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
Toast.makeText(getApplicationContext(), "Downloading File", Toast.LENGTH_LONG).show();
}
Toast.makeText(getApplicationContext(), "Downloaded File", Toast.LENGTH_LONG).show();
} else {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setMimeType(mime_type);
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Downloading file...");
request.setTitle(file_name);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, URLUtil.guessFileName(url, content_disposition, mime_type));
DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
Toast.makeText(getApplicationContext(), "Downloading File", Toast.LENGTH_LONG).show();
}
}
});
});
web_view.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
web_view.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
result.confirm();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(defaultValue);
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
/*
** https://stackoverflow.com/questions/5907369/file-upload-in-webview
** https://stackoverflow.com/questions/8586691/how-to-open-file-save-dialog-in-android
*/
@Override
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> message, WebChromeClient.FileChooserParams params) {
upload_message = message;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
TildeFriendsActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), TildeFriendsActivity.FILECHOOSER_RESULT);
return true;
}
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d("tildefriends", consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
}
});
web_view.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
{
if (request.getUrl() != null && request.getUrl().toString().startsWith(base_url)) {
return false;
} else {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, request.getUrl()));
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.create()
.show();
return true;
}
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(defaultValue);
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
/*
** https://stackoverflow.com/questions/5907369/file-upload-in-webview
** https://stackoverflow.com/questions/8586691/how-to-open-file-save-dialog-in-android
*/
@Override
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> message, WebChromeClient.FileChooserParams params) {
upload_message = message;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
TildeFriendsActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), TildeFriendsActivity.FILECHOOSER_RESULT);
return true;
}
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d("tildefriends", consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
}
});
web_view.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
{
if (request.getUrl() != null && request.getUrl().toString().startsWith(base_url)) {
return false;
} else {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, request.getUrl()));
return true;
}
}
});
});
s_activity.create_thread = null;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
super.onCreate(savedInstanceState);
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDialog()
.penaltyLog()
.build());
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.detectAll()
.penaltyLog()
.build());
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
TextView refresh = (TextView)findViewById(R.id.refresh);
refresh.setVisibility(View.GONE);
refresh.setText("REFRESH");
create_thread = new Thread(new Runnable() {
@Override
public void run() {
s_activity.createThread();
}
});
create_thread.start();
}
@Override
@@ -403,7 +426,7 @@ public class TildeFriendsActivity extends Activity {
Log.w("tildefriends", "onServiceDisconnected");
}
};
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE);
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND);
}
public static void stop_sandbox() {
@@ -422,6 +445,7 @@ public class TildeFriendsActivity extends Activity {
hide_status();
web_view.loadUrl(base_url + "login/auto");
});
observer.stopWatching();
observer = null;
} else {
runOnUiThread(() -> {
@@ -451,4 +475,10 @@ public class TildeFriendsActivity extends Activity {
web_view.getSettings().setDatabasePath(getDatabasePath("webview").getPath());
}
}
@SuppressWarnings("deprecation")
private void set_database_enabled()
{
web_view.getSettings().setDatabaseEnabled(true);
}
}

View File

@@ -3,7 +3,9 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
android:fitsSystemWindows="true"
android:background="#000">
<com.unprompted.tildefriends.TildeFriendsWebView
android:id="@+id/web"
android:layout_width="match_parent"

View File

@@ -1,11 +1,159 @@
#include "api.js.h"
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include <quickjs.h>
typedef struct _app_path_pair_t
{
const char* app;
const char* path;
} app_path_pair_t;
typedef struct _get_apps_t
{
app_path_pair_t* apps;
int count;
JSContext* context;
JSValue promise[2];
char user[];
} get_apps_t;
static void _tf_api_core_apps_work(tf_ssb_t* ssb, void* user_data)
{
get_apps_t* work = user_data;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* apps = tf_ssb_db_get_property(ssb, work->user, "apps");
if (apps)
{
JSValue apps_array = JS_ParseJSON(context, apps, strlen(apps), NULL);
if (JS_IsArray(context, apps_array))
{
int length = tf_util_get_length(context, apps_array);
for (int i = 0; i < length; i++)
{
JSValue name = JS_GetPropertyUint32(context, apps_array, i);
const char* name_string = JS_ToCString(context, name);
if (name_string)
{
work->apps = tf_resize_vec(work->apps, sizeof(app_path_pair_t) * (work->count + 1));
work->apps[work->count].app = tf_strdup(name_string);
size_t size = strlen("path:") + strlen(name_string) + 1;
char* path_key = tf_malloc(size);
snprintf(path_key, size, "path:%s", name_string);
work->apps[work->count].path = tf_ssb_db_get_property(ssb, work->user, path_key);
tf_free(path_key);
work->count++;
}
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
}
}
JS_FreeValue(context, apps_array);
}
tf_free((void*)apps);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_apps_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_NewObject(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyStr(context, result, work->apps[i].app, JS_NewString(context, work->apps[i].path));
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
for (int i = 0; i < work->count; i++)
{
tf_free((void*)work->apps[i].app);
tf_free((void*)work->apps[i].path);
}
tf_free(work->apps);
tf_free(work);
}
static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue user = argv[0];
JSValue process = data[0];
const char* user_string = JS_IsString(user) ? JS_ToCString(context, user) : NULL;
if (JS_IsObject(process))
{
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
if (JS_IsObject(credentials))
{
JSValue session = JS_GetPropertyStr(context, credentials, "session");
if (JS_IsObject(session))
{
JSValue session_name = JS_GetPropertyStr(context, session, "name");
const char* session_name_string = JS_IsString(session_name) ? JS_ToCString(context, session_name) : NULL;
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
JS_FreeValue(context, session_name);
}
JS_FreeValue(context, session);
}
JS_FreeValue(context, credentials);
}
if (user_string)
{
get_apps_t* work = tf_malloc(sizeof(get_apps_t) + strlen(user_string) + 1);
*work = (get_apps_t) {
.context = context,
};
memcpy(work->user, user_string, strlen(user_string) + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_ssb_run_work(ssb, _tf_api_core_apps_work, _tf_api_core_apps_after_work, work);
}
else
{
result = JS_NewObject(context);
}
JS_FreeCString(context, user_string);
return result;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue imports = argv[0];
JSValue process = argv[1];
JSValue core = JS_GetPropertyStr(context, imports, "core");
JS_SetPropertyStr(context, core, "apps", JS_NewCFunctionData(context, _tf_api_core_apps, 1, 0, 1, &process));
JS_FreeValue(context, core);
return JS_UNDEFINED;
}

View File

@@ -813,6 +813,11 @@ void tf_http_destroy(tf_http_t* http)
return;
}
if (!http->is_shutting_down)
{
tf_printf("tf_http_destroy\n");
}
http->is_shutting_down = true;
http->is_in_destroy = true;

252
src/httpd.app.c Normal file
View File

@@ -0,0 +1,252 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include "picohttpparser.h"
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _app_blob_t
{
tf_http_request_t* request;
bool found;
bool not_modified;
bool use_handler;
bool use_static;
void* data;
size_t size;
char app_blob_id[k_blob_id_len];
const char* file;
tf_httpd_user_app_t* user_app;
char etag[256];
} app_blob_t;
static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data)
{
app_blob_t* data = user_data;
tf_http_request_t* request = data->request;
if (request->path[0] == '/' && request->path[1] == '~')
{
const char* last_slash = strchr(request->path + 1, '/');
if (last_slash)
{
last_slash = strchr(last_slash + 1, '/');
}
data->user_app = last_slash ? tf_httpd_parse_user_app_from_path(request->path, last_slash) : NULL;
if (data->user_app)
{
size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", data->user_app->app);
const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path);
tf_string_set(data->app_blob_id, sizeof(data->app_blob_id), value);
tf_free(app_path);
tf_free((void*)value);
data->file = last_slash + 1;
}
}
else if (request->path[0] == '/' && request->path[1] == '&')
{
const char* end = strstr(request->path, ".sha256/");
if (end)
{
snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1);
data->file = end + strlen(".sha256/");
}
}
char* app_blob = NULL;
size_t app_blob_size = 0;
if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size))
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL);
JSValue files = JS_GetPropertyStr(context, app_object, "files");
JSValue blob_id = JS_GetPropertyStr(context, files, data->file);
if (JS_IsUndefined(blob_id))
{
blob_id = JS_GetPropertyStr(context, files, "handler.js");
if (!JS_IsUndefined(blob_id))
{
data->use_handler = true;
}
}
else
{
const char* blob_id_str = JS_ToCString(context, blob_id);
if (blob_id_str)
{
snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str);
const char* match = tf_http_request_get_header(data->request, "if-none-match");
if (match && strcmp(match, data->etag) == 0)
{
data->not_modified = true;
}
else
{
data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size);
}
}
JS_FreeCString(context, blob_id_str);
}
JS_FreeValue(context, blob_id);
JS_FreeValue(context, files);
JS_FreeValue(context, app_object);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
tf_free(app_blob);
}
}
static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler");
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue handler_blob_id = JS_NewString(context, app_blob_id);
JSValue path_value = JS_NewString(context, path);
JSValue package_owner_value = JS_NewString(context, package_owner);
JSValue app_value = JS_NewString(context, app);
JSValue query_value = request->query ? JS_NewString(context, request->query) : JS_UNDEFINED;
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
char name[256] = "";
snprintf(name, sizeof(name), "%.*s", (int)request->headers[i].name_len, request->headers[i].name);
JS_SetPropertyStr(context, headers, name, JS_NewStringLen(context, request->headers[i].value, request->headers[i].value_len));
}
JSValue args[] = {
response,
handler_blob_id,
path_value,
query_value,
headers,
package_owner_value,
app_value,
};
JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, headers);
JS_FreeValue(context, query_value);
JS_FreeValue(context, app_value);
JS_FreeValue(context, package_owner_value);
JS_FreeValue(context, handler_blob_id);
JS_FreeValue(context, path_value);
JS_FreeValue(context, response);
JS_FreeValue(context, call_app_handler);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}
static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_blob_t* data = user_data;
if (data->not_modified)
{
tf_http_respond(data->request, 304, NULL, 0, NULL, 0);
}
else if (data->use_static)
{
tf_httpd_endpoint_static(data->request);
}
else if (data->use_handler)
{
_httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app->user, data->user_app->app);
}
else if (data->found)
{
const char* mime_type = tf_httpd_ext_to_content_type(strrchr(data->request->path, '.'), false);
if (!mime_type)
{
mime_type = tf_httpd_magic_bytes_to_content_type(data->data, data->size);
}
const char* headers[] = {
"Access-Control-Allow-Origin",
"*",
"Content-Security-Policy",
"sandbox allow-downloads allow-top-navigation-by-user-activation",
"Content-Type",
mime_type ? mime_type : "application/binary",
"etag",
data->etag,
};
tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size);
}
tf_free(data->user_app);
tf_free(data->data);
tf_http_request_unref(data->request);
tf_free(data);
}
void tf_httpd_endpoint_app(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
app_blob_t* data = tf_malloc(sizeof(app_blob_t));
*data = (app_blob_t) { .request = request };
tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data);
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response,
};
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}

91
src/httpd.delete.c Normal file
View File

@@ -0,0 +1,91 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
typedef struct _delete_t
{
tf_http_request_t* request;
const char* session;
int response;
} delete_t;
static void _httpd_endpoint_delete_work(tf_ssb_t* ssb, void* user_data)
{
delete_t* delete = user_data;
tf_http_request_t* request = delete->request;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, delete->session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && tf_httpd_is_name_valid(user_string))
{
tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/delete");
if (user_app)
{
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, user_string, "administration")))
{
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", user_app->app);
bool changed = false;
changed = tf_ssb_db_remove_value_from_array_property(ssb, user_string, "apps", user_app->app) || changed;
changed = tf_ssb_db_remove_property(ssb, user_string, app_path) || changed;
delete->response = changed ? 200 : 404;
tf_free(app_path);
}
else
{
delete->response = 401;
}
}
else
{
delete->response = 404;
}
tf_free(user_app);
}
else
{
delete->response = 401;
}
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_delete_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_t* delete = user_data;
const char* k_payload = tf_http_status_text(delete->response ? delete->response : 404);
tf_http_respond(delete->request, delete->response ? delete->response : 404, NULL, 0, k_payload, strlen(k_payload));
tf_http_request_unref(delete->request);
tf_free((void*)delete->session);
tf_free(delete);
}
void tf_httpd_endpoint_delete(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
delete_t* delete = tf_malloc(sizeof(delete_t));
*delete = (delete_t) {
.request = request,
.session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session"),
};
tf_ssb_run_work(ssb, _httpd_endpoint_delete_work, _httpd_endpoint_delete_after_work, delete);
}

233
src/httpd.index.c Normal file
View File

@@ -0,0 +1,233 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _index_t
{
tf_http_request_t* request;
bool found;
bool not_modified;
bool use_handler;
bool use_static;
void* data;
size_t size;
char app_blob_id[k_blob_id_len];
const char* file;
tf_httpd_user_app_t* user_app;
char etag[256];
} index_t;
static bool _has_property(JSContext* context, JSValue object, const char* name)
{
JSAtom atom = JS_NewAtom(context, name);
bool result = JS_HasProperty(context, object, atom) > 0;
JS_FreeAtom(context, atom);
return result;
}
static void _httpd_endpoint_app_index_work(tf_ssb_t* ssb, void* user_data)
{
index_t* data = user_data;
data->use_static = true;
tf_httpd_user_app_t* user_app = data->user_app;
size_t app_path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(app_path_length);
snprintf(app_path, app_path_length, "path:%s", user_app->app);
const char* app_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path);
tf_free(app_path);
uint8_t* app_blob = NULL;
size_t app_blob_size = 0;
if (tf_ssb_db_blob_get(ssb, app_blob_id, &app_blob, &app_blob_size))
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue app = JS_ParseJSON(context, (const char*)app_blob, app_blob_size, NULL);
JSValue files = JS_GetPropertyStr(context, app, "files");
if (!_has_property(context, files, "app.js"))
{
JSValue index = JS_GetPropertyStr(context, files, "index.html");
if (JS_IsString(index))
{
const char* index_string = JS_ToCString(context, index);
tf_ssb_db_blob_get(ssb, index_string, (uint8_t**)&data->data, &data->size);
JS_FreeCString(context, index_string);
}
JS_FreeValue(context, index);
}
JS_FreeValue(context, files);
JS_FreeValue(context, app);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
tf_free(app_blob);
}
tf_free((void*)app_blob_id);
}
static char* _replace(const char* original, size_t size, const char* find, const char* replace, size_t* out_size)
{
char* pos = strstr(original, find);
if (!pos)
{
return tf_strdup(original);
}
size_t replace_length = strlen(replace);
size_t find_length = strlen(find);
size_t new_size = size + replace_length - find_length;
char* buffer = tf_malloc(new_size);
memcpy(buffer, original, pos - original);
memcpy(buffer + (pos - original), replace, replace_length);
memcpy(buffer + (pos - original) + replace_length, pos + find_length, size - (pos - original) - find_length);
*out_size = new_size;
return buffer;
}
static char* _append_raw(char* document, size_t* current_size, const char* data, size_t size)
{
document = tf_resize_vec(document, *current_size + size);
memcpy(document + *current_size, data, size);
document[*current_size + size] = '\0';
*current_size += size;
return document;
}
static char* _append_encoded(char* document, const char* data, size_t size, size_t* out_size)
{
size_t current_size = strlen(document);
int accum = 0;
for (int i = 0; (size_t)i < size; i++)
{
switch (data[i])
{
case '"':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&quot;", strlen("&quot;"));
accum = i + 1;
break;
case '\'':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&#x27;", strlen("&#x27;"));
accum = i + 1;
break;
case '<':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&lt;", strlen("&lt;"));
accum = i + 1;
break;
case '>':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&gt;", strlen("&gt;"));
accum = i + 1;
break;
case '&':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&amp;", strlen("&amp;"));
accum = i + 1;
break;
default:
break;
}
}
*out_size = current_size;
return document;
}
static void _httpd_endpoint_app_index_file_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
index_t* state = user_data;
if (result > 0)
{
char* replacement = tf_strdup("<iframe srcdoc=\"");
size_t replacement_size = 0;
replacement = _append_encoded(replacement, state->data, state->size, &replacement_size);
replacement = _append_raw(replacement, &replacement_size, "\"", 1);
size_t size = 0;
char* document = _replace(data, result, "<iframe", replacement, &size);
const char* headers[] = {
"Content-Type",
"text/html; charset=utf-8",
};
tf_http_respond(state->request, 200, headers, tf_countof(headers) / 2, document, size);
tf_free(replacement);
tf_free(document);
}
tf_free(state->data);
tf_free(state->user_app);
tf_http_request_unref(state->request);
tf_free(state);
}
static void _httpd_endpoint_app_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
index_t* data = user_data;
if (data->data)
{
tf_task_t* task = data->request->user_data;
const char* root_path = tf_task_get_root_path(task);
size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen("core/index.html") + 1;
char* path = alloca(size);
snprintf(path, size, "%s%score/index.html", root_path ? root_path : "", root_path ? "/" : "");
tf_file_read(task, path, _httpd_endpoint_app_index_file_read, data);
}
else
{
tf_httpd_endpoint_static(data->request);
tf_free(data->user_app);
tf_http_request_unref(data->request);
tf_free(data);
}
}
void tf_httpd_endpoint_app_index(tf_http_request_t* request)
{
tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/");
if (!user_app)
{
return tf_httpd_endpoint_static(request);
}
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
index_t* data = tf_malloc(sizeof(index_t));
(*data) = (index_t) { .request = request, .user_app = user_app };
tf_http_request_ref(request);
tf_ssb_run_work(ssb, _httpd_endpoint_app_index_work, _httpd_endpoint_app_index_after_work, data);
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,43 @@
** @{
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "quickjs.h"
static const int64_t k_httpd_auth_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
/** A JS context. */
typedef struct JSContext JSContext;
/**
** An HTTP server instance.
*/
typedef struct _tf_http_t tf_http_t;
/**
** An HTTP request.
*/
typedef struct _tf_http_request_t tf_http_request_t;
/**
** An SSB instance.
*/
typedef struct _tf_ssb_t tf_ssb_t;
/**
** A user and app name.
*/
typedef struct _tf_httpd_user_app_t
{
/** The username. */
const char* user;
/** The app name. */
const char* app;
} tf_httpd_user_app_t;
/**
** Register the HTTP script interface. Also registers a number of built-in
** request handlers. An ongoing project is to move the JS request handlers
@@ -38,4 +68,153 @@ tf_http_t* tf_httpd_create(JSContext* context);
*/
void tf_httpd_destroy(tf_http_t* http);
/**
** Determine a content-type from a file extension.
** @param ext The file extension.
** @param use_fallback If not found, fallback to application/binary.
** @return A MIME type or NULL.
*/
const char* tf_httpd_ext_to_content_type(const char* ext, bool use_fallback);
/**
** Determine a content type from magic bytes.
** @param bytes The first bytes of a file.
** @param size The length of the bytes.
** @return A MIME type or NULL.
*/
const char* tf_httpd_magic_bytes_to_content_type(const uint8_t* bytes, size_t size);
/**
** Respond with a redirect.
** @param request The HTTP request.
** @return true if redirected.
*/
bool tf_httpd_redirect(tf_http_request_t* request);
/**
** Parse a username and app from a path like /~user/app/.
** @param path The path.
** @param expected_suffix A suffix that is required to be on the path, and removed.
** @return The user and app. Free with tf_free().
*/
tf_httpd_user_app_t* tf_httpd_parse_user_app_from_path(const char* path, const char* expected_suffix);
/**
** Decode form data into key value pairs.
** @param data The form data string.
** @param length The length of the form data string.
** @return Key values pairs terminated by NULL.
*/
const char** tf_httpd_form_data_decode(const char* data, int length);
/**
** Get a form data value from an array of key value pairs produced by tf_httpd_form_data_decode().
** @param form_data The form data.
** @param key The key for which to fetch the value.
** @return the value for the case-insensitive key or NULL.
*/
const char* tf_httpd_form_data_get(const char** form_data, const char* key);
/**
** Validate a JWT.
** @param ssb The SSB instance.
** @param context A JS context.
** @param jwt The JWT.
** @return The JWT contents if valid.
*/
JSValue tf_httpd_authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
;
/**
** Make a JS response object for a request.
** @param context The JS context.
** @param request The HTTP request.
** @return The respone object.
*/
JSValue tf_httpd_make_response_object(JSContext* context, tf_http_request_t* request);
/**
** Check if a name meets requirements.
** @param name The name.
** @return true if the name is valid.
*/
bool tf_httpd_is_name_valid(const char* name);
/**
** Make a header for the session cookie.
** @param request The HTTP request.
** @param session_cookie The session cookie.
** @return The header.
*/
const char* tf_httpd_make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
/**
** Make a JWT for the session.
** @param context A JS context.
** @param ssb The SSB instance.
** @param name The username.
** @return The JWT.
*/
const char* tf_httpd_make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
/**
** Serve a static file.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_static(tf_http_request_t* request);
/**
** View a blob.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_view(tf_http_request_t* request);
/**
** Save a blob or app.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_save(tf_http_request_t* request);
/**
** Delete a blob or app.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_delete(tf_http_request_t* request);
/**
** App endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app(tf_http_request_t* request);
/**
** App index endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app_index(tf_http_request_t* request);
/**
** App WebSocket.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app_socket(tf_http_request_t* request);
/**
** Login endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_login(tf_http_request_t* request);
/**
** Auto-login endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_login_auto(tf_http_request_t* request);
/**
** Logout endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_logout(tf_http_request_t* request);
/** @} */

537
src/httpd.login.c Normal file
View File

@@ -0,0 +1,537 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include "ow-crypt.h"
#include "sodium/utils.h"
#include <inttypes.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _login_request_t
{
tf_http_request_t* request;
const char* name;
const char* error;
const char* settings;
const char* code_of_conduct;
bool have_administrator;
bool session_is_new;
char location_header[1024];
const char* set_cookie_header;
int pending;
} login_request_t;
const char* tf_httpd_make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
{
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
int length = session_cookie ? snprintf(NULL, 0, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
char* cookie = length ? tf_malloc(length + 1) : NULL;
if (cookie)
{
snprintf(cookie, length + 1, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "");
}
return cookie;
}
static void _login_release(login_request_t* login)
{
int ref_count = --login->pending;
if (ref_count == 0)
{
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->set_cookie_header);
tf_free(login);
}
}
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (result >= 0)
{
const char* headers[] = {
"Content-Type",
"text/html; charset=utf-8",
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
const char* replace_me = "$AUTH_DATA";
const char* auth = strstr(data, replace_me);
if (auth)
{
JSContext* context = tf_task_get_context(task);
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "session_is_new", JS_NewBool(context, login->session_is_new));
JS_SetPropertyStr(context, object, "name", login->name ? JS_NewString(context, login->name) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "error", login->error ? JS_NewString(context, login->error) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "code_of_conduct", login->code_of_conduct ? JS_NewString(context, login->code_of_conduct) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "have_administrator", JS_NewBool(context, login->have_administrator));
JSValue object_json = JS_JSONStringify(context, object, JS_NULL, JS_NULL);
size_t json_length = 0;
const char* json = JS_ToCStringLen(context, &json_length, object_json);
char* copy = tf_malloc(result + json_length);
int replace_start = (auth - (const char*)data);
int replace_end = (auth - (const char*)data) + (int)strlen(replace_me);
memcpy(copy, data, replace_start);
memcpy(copy + replace_start, json, json_length);
memcpy(copy + replace_start + json_length, ((const char*)data) + replace_end, result - replace_end);
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, copy, replace_start + json_length + (result - replace_end));
tf_free(copy);
JS_FreeCString(context, json);
JS_FreeValue(context, object_json);
JS_FreeValue(context, object);
}
else
{
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
_login_release(login);
}
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
{
bool result = false;
JSValue user = JS_GetPropertyStr(context, session, "name");
const char* user_string = JS_ToCString(context, user);
result = user_string && strcmp(user_string, "guest") != 0;
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
return result;
}
static bool _make_administrator_if_first(tf_ssb_t* ssb, JSContext* context, const char* account_name_copy, bool may_become_first_admin)
{
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings && *settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
if (JS_IsUndefined(settings_value))
{
settings_value = JS_NewObject(context);
}
bool have_administrator = false;
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
for (int i = 0; i < (int)plen; i++)
{
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
{
int permission_length = tf_util_get_length(context, desc.value);
for (int i = 0; i < permission_length; i++)
{
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
const char* permission = JS_ToCString(context, entry);
if (permission && strcmp(permission, "administration") == 0)
{
have_administrator = true;
}
JS_FreeCString(context, permission);
JS_FreeValue(context, entry);
}
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
JS_FreeValue(context, desc.value);
}
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
if (!have_administrator && may_become_first_admin)
{
if (JS_IsUndefined(permissions))
{
permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "permissions", JS_DupValue(context, permissions));
}
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
if (JS_IsUndefined(user))
{
user = JS_NewArray(context);
JS_SetPropertyStr(context, permissions, account_name_copy, JS_DupValue(context, user));
}
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
JS_FreeValue(context, user);
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
return have_administrator;
}
static bool _verify_password(const char* password, const char* hash)
{
char buffer[7 + 22 + 31 + 1];
const char* out_hash = crypt_rn(password, hash, buffer, sizeof(buffer));
return out_hash && strcmp(hash, out_hash) == 0;
}
static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
const char** form_data = tf_httpd_form_data_decode(request->query, request->query ? strlen(request->query) : 0);
const char* account_name_copy = NULL;
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
if (_session_is_authenticated_as_user(context, jwt))
{
const char* return_url = tf_httpd_form_data_get(form_data, "return");
if (return_url)
{
tf_string_set(login->location_header, sizeof(login->location_header), return_url);
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
goto done;
}
const char* send_session = tf_strdup(session);
bool session_is_new = false;
const char* login_error = NULL;
bool may_become_first_admin = false;
if (strcmp(request->method, "POST") == 0)
{
session_is_new = true;
const char** post_form_data = tf_httpd_form_data_decode(request->body, request->content_length);
const char* submit = tf_httpd_form_data_get(post_form_data, "submit");
if (submit && strcmp(submit, "Login") == 0)
{
const char* account_name = tf_httpd_form_data_get(post_form_data, "name");
account_name_copy = tf_strdup(account_name);
const char* password = tf_httpd_form_data_get(post_form_data, "password");
const char* new_password = tf_httpd_form_data_get(post_form_data, "new_password");
const char* confirm = tf_httpd_form_data_get(post_form_data, "confirm");
const char* change = tf_httpd_form_data_get(post_form_data, "change");
const char* form_register = tf_httpd_form_data_get(post_form_data, "register");
char account_passwd[256] = { 0 };
bool have_account = tf_ssb_db_get_account_password_hash(ssb, tf_httpd_form_data_get(post_form_data, "name"), account_passwd, sizeof(account_passwd));
if (form_register && strcmp(form_register, "1") == 0)
{
bool registered = false;
if (!tf_httpd_is_name_valid(account_name))
{
login_error = "Invalid username. Usernames must contain only letters from the English alphabet and digits and must start with a letter.";
}
else
{
if (!have_account && tf_httpd_is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password);
tf_ssb_release_db_writer(ssb, db);
if (registered)
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
}
}
if (!registered && !login_error)
{
login_error = "Error registering account.";
}
}
else if (change && strcmp(change, "1") == 0)
{
bool set = false;
if (have_account && tf_httpd_is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 &&
_verify_password(password, account_passwd))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
set = tf_ssb_db_set_account_password(tf_ssb_get_loop(ssb), db, context, account_name, new_password);
tf_ssb_release_db_writer(ssb, db);
if (set)
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
}
}
if (!set)
{
login_error = "Error changing password.";
}
}
else
{
if (have_account && *account_passwd && _verify_password(password, account_passwd))
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
else
{
login_error = "Invalid username or password.";
}
}
}
else
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, "guest");
}
tf_free(post_form_data);
}
bool have_administrator = _make_administrator_if_first(ssb, context, account_name_copy, may_become_first_admin);
if (session_is_new && tf_httpd_form_data_get(form_data, "return") && !login_error)
{
const char* return_url = tf_httpd_form_data_get(form_data, "return");
if (return_url)
{
tf_string_set(login->location_header, sizeof(login->location_header), return_url);
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
login->set_cookie_header = tf_httpd_make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
}
else
{
login->name = account_name_copy;
login->error = login_error;
login->set_cookie_header = tf_httpd_make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
login->session_is_new = session_is_new;
login->have_administrator = have_administrator;
login->settings = tf_ssb_db_get_property(ssb, "core", "settings");
if (login->settings)
{
JSValue settings_value = JS_ParseJSON(context, login->settings, strlen(login->settings), NULL);
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
const char* result = tf_strdup(code_of_conduct);
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)login->settings);
login->settings = NULL;
login->code_of_conduct = result;
}
login->pending++;
tf_http_request_ref(request);
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
account_name_copy = NULL;
}
done:
tf_free((void*)session);
tf_free(form_data);
tf_free((void*)account_name_copy);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (login->pending == 1)
{
if (*login->location_header)
{
const char* headers[] = {
"Location",
login->location_header,
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
}
tf_http_request_unref(request);
_login_release(login);
}
void tf_httpd_endpoint_login(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t) {
.request = request,
};
login->pending++;
tf_ssb_run_work(ssb, _httpd_endpoint_login_work, _httpd_endpoint_login_after_work, login);
}
void tf_httpd_endpoint_logout(tf_http_request_t* request)
{
const char* k_set_cookie = request->is_tls ? "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly"
: "session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
const char* k_location_format = "/login%s%s";
int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
char* location = alloca(length + 1);
snprintf(location, length + 1, k_location_format, request->query ? "?" : "", request->query ? request->query : "");
const char* headers[] = {
"Set-Cookie",
k_set_cookie,
"Location",
location,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
typedef struct _auto_login_t
{
tf_http_request_t* request;
bool autologin;
const char* users;
} auto_login_t;
static void _httpd_auto_login_work(tf_ssb_t* ssb, void* user_data)
{
auto_login_t* request = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_bool(db, "autologin", &request->autologin);
tf_ssb_release_db_reader(ssb, db);
if (request->autologin)
{
request->users = tf_ssb_db_get_property(ssb, "auth", "users");
if (request->users && strcmp(request->users, "[]") == 0)
{
tf_free((void*)request->users);
request->users = NULL;
}
if (!request->users)
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
static const char* k_account_name = "mobile";
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
bool registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, k_account_name, k_account_name);
tf_ssb_release_db_writer(ssb, db);
if (registered)
{
_make_administrator_if_first(ssb, context, k_account_name, true);
}
JS_FreeContext(context);
JS_FreeRuntime(runtime);
request->users = tf_ssb_db_get_property(ssb, "auth", "users");
}
}
}
static void _httpd_auto_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
auto_login_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
const char* session_token = NULL;
if (!work->autologin)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(work->request, 404, NULL, 0, k_payload, strlen(k_payload));
}
else
{
if (work->users)
{
JSValue json = JS_ParseJSON(context, work->users, strlen(work->users), NULL);
JSValue user = JS_GetPropertyUint32(context, json, 0);
const char* user_string = JS_ToCString(context, user);
session_token = tf_httpd_make_session_jwt(context, ssb, user_string);
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, json);
}
if (session_token)
{
const char* cookie = tf_httpd_make_set_session_cookie_header(work->request, session_token);
tf_free((void*)session_token);
const char* headers[] = {
"Set-Cookie",
cookie,
"Location",
"/",
};
tf_http_respond(work->request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_free((void*)cookie);
}
else
{
const char* headers[] = {
"Location",
"/",
};
tf_http_respond(work->request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
}
tf_http_request_unref(work->request);
tf_free((void*)work->users);
tf_free(work);
}
void tf_httpd_endpoint_login_auto(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
auto_login_t* work = tf_malloc(sizeof(auto_login_t));
*work = (auto_login_t) { .request = request };
tf_ssb_run_work(ssb, _httpd_auto_login_work, _httpd_auto_login_after_work, work);
}

181
src/httpd.save.c Normal file
View File

@@ -0,0 +1,181 @@
#include "httpd.js.h"
#include "http.h"
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
typedef struct _save_t
{
tf_http_request_t* request;
int response;
char blob_id[k_blob_id_len];
} save_t;
static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
{
save_t* save = user_data;
tf_http_request_t* request = save->request;
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && tf_httpd_is_name_valid(user_string))
{
tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/save");
if (user_app)
{
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, user_string, "administration")))
{
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", user_app->app);
const char* old_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path);
JSValue new_app = JS_ParseJSON(context, request->body, request->content_length, NULL);
tf_util_report_error(context, new_app);
if (JS_IsObject(new_app))
{
uint8_t* old_blob = NULL;
size_t old_blob_size = 0;
if (tf_ssb_db_blob_get(ssb, old_blob_id, &old_blob, &old_blob_size))
{
JSValue old_app = JS_ParseJSON(context, (const char*)old_blob, old_blob_size, NULL);
if (JS_IsObject(old_app))
{
JSAtom previous = JS_NewAtom(context, "previous");
JS_DeleteProperty(context, old_app, previous, 0);
JS_DeleteProperty(context, new_app, previous, 0);
JSValue old_app_json = JS_JSONStringify(context, old_app, JS_NULL, JS_NULL);
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
const char* old_app_str = JS_ToCString(context, old_app_json);
const char* new_app_str = JS_ToCString(context, new_app_json);
if (old_app_str && new_app_str && strcmp(old_app_str, new_app_str) == 0)
{
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", old_blob_id);
save->response = 200;
}
JS_FreeCString(context, old_app_str);
JS_FreeCString(context, new_app_str);
JS_FreeValue(context, old_app_json);
JS_FreeValue(context, new_app_json);
JS_FreeAtom(context, previous);
}
JS_FreeValue(context, old_app);
tf_free(old_blob);
}
if (!save->response)
{
if (old_blob_id)
{
JS_SetPropertyStr(context, new_app, "previous", JS_NewString(context, old_blob_id));
}
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
size_t new_app_length = 0;
const char* new_app_str = JS_ToCStringLen(context, &new_app_length, new_app_json);
char blob_id[k_blob_id_len] = { 0 };
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)new_app_str, new_app_length, blob_id, sizeof(blob_id), NULL) &&
tf_ssb_db_set_property(ssb, user_app->user, app_path, blob_id))
{
tf_ssb_db_add_value_to_array_property(ssb, user_app->user, "apps", user_app->app);
tf_string_set(save->blob_id, sizeof(save->blob_id), blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store or property set failed.\n");
save->response = 500;
}
JS_FreeCString(context, new_app_str);
JS_FreeValue(context, new_app_json);
}
}
else
{
save->response = 400;
}
JS_FreeValue(context, new_app);
tf_free(app_path);
tf_free((void*)old_blob_id);
}
else
{
save->response = 403;
}
tf_free(user_app);
}
else if (strcmp(request->path, "/save") == 0)
{
char blob_id[k_blob_id_len] = { 0 };
if (tf_ssb_db_blob_store(ssb, request->body, request->content_length, blob_id, sizeof(blob_id), NULL))
{
tf_string_set(save->blob_id, sizeof(save->blob_id), blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store failed.\n");
save->response = 500;
}
}
else
{
save->response = 400;
}
}
else
{
save->response = 401;
}
tf_free((void*)session);
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_save_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
save_t* save = user_data;
tf_http_request_t* request = save->request;
if (*save->blob_id)
{
char body[256] = "";
int length = snprintf(body, sizeof(body), "/%s", save->blob_id);
tf_http_respond(request, 200, NULL, 0, body, length);
}
tf_http_request_unref(request);
tf_free(save);
}
void tf_httpd_endpoint_save(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
save_t* save = tf_malloc(sizeof(save_t));
*save = (save_t) {
.request = request,
};
tf_ssb_run_work(ssb, _httpd_endpoint_save_work, _httpd_endpoint_save_after_work, save);
}

202
src/httpd.static.c Normal file
View File

@@ -0,0 +1,202 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "task.h"
#include "util.js.h"
#include <assert.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _http_file_t
{
tf_http_request_t* request;
char etag[512];
} http_file_t;
static bool _ends_with(const char* a, const char* suffix)
{
if (!a || !suffix)
{
return false;
}
size_t alen = strlen(a);
size_t suffixlen = strlen(suffix);
return alen >= suffixlen && strcmp(a + alen - suffixlen, suffix) == 0;
}
static const char* _after(const char* text, const char* prefix)
{
size_t prefix_length = strlen(prefix);
if (text && strncmp(text, prefix, prefix_length) == 0)
{
return text + prefix_length;
}
return NULL;
}
static double _time_spec_to_double(const uv_timespec_t* time_spec)
{
return (double)time_spec->tv_sec + (double)(time_spec->tv_nsec) / 1e9;
}
static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
http_file_t* file = user_data;
tf_http_request_t* request = file->request;
if (result >= 0)
{
if (strcmp(path, "core/tfrpc.js") == 0 || _ends_with(path, "core/tfrpc.js"))
{
const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
"etag",
file->etag,
"Access-Control-Allow-Origin",
"null",
};
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
else
{
const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
"etag",
file->etag,
};
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
tf_free(file);
}
static void _httpd_endpoint_static_stat(tf_task_t* task, const char* path, int result, const uv_stat_t* stat, void* user_data)
{
tf_http_request_t* request = user_data;
const char* match = tf_http_request_get_header(request, "if-none-match");
if (result != 0)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
tf_http_request_unref(request);
}
else
{
char etag[512];
snprintf(etag, sizeof(etag), "\"%f_%zd\"", _time_spec_to_double(&stat->st_mtim), (size_t)stat->st_size);
if (match && strcmp(match, etag) == 0)
{
tf_http_respond(request, 304, NULL, 0, NULL, 0);
tf_http_request_unref(request);
}
else
{
http_file_t* file = tf_malloc(sizeof(http_file_t));
*file = (http_file_t) { .request = request };
static_assert(sizeof(file->etag) == sizeof(etag), "Size mismatch");
memcpy(file->etag, etag, sizeof(etag));
tf_file_read(task, path, _httpd_endpoint_static_read, file);
}
}
}
void tf_httpd_endpoint_static(tf_http_request_t* request)
{
if (request->path && strncmp(request->path, "/.well-known/", strlen("/.well-known/")) && tf_httpd_redirect(request))
{
return;
}
const char* k_static_files[] = {
"index.html",
"client.js",
"tildefriends.svg",
"jszip.min.js",
"style.css",
"tfrpc.js",
"w3.css",
};
const char* k_map[][2] = {
{ "/static/", "core/" },
{ "/lit/", "deps/lit/" },
{ "/codemirror/", "deps/codemirror/" },
{ "/prettier/", "deps/prettier/" },
{ "/speedscope/", "deps/speedscope/" },
{ "/.well-known/", "data/global/.well-known/" },
};
bool is_core = false;
const char* after = NULL;
const char* file_path = NULL;
for (int i = 0; i < tf_countof(k_map) && !after; i++)
{
const char* next_after = _after(request->path, k_map[i][0]);
if (next_after)
{
after = next_after;
file_path = k_map[i][1];
is_core = after && i == 0;
}
}
if ((!after || !*after) && request->path[strlen(request->path) - 1] == '/')
{
after = "index.html";
if (!file_path)
{
file_path = "core/";
is_core = true;
}
}
if (!after || strstr(after, ".."))
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
return;
}
if (is_core)
{
bool found = false;
for (int i = 0; i < tf_countof(k_static_files); i++)
{
if (strcmp(after, k_static_files[i]) == 0)
{
found = true;
break;
}
}
if (!found)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
return;
}
}
tf_task_t* task = request->user_data;
const char* root_path = tf_task_get_root_path(task);
size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen(file_path) + strlen(after) + 1;
char* path = alloca(size);
snprintf(path, size, "%s%s%s%s", root_path ? root_path : "", root_path ? "/" : "", file_path, after);
tf_http_request_ref(request);
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}

140
src/httpd.view.c Normal file
View File

@@ -0,0 +1,140 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
typedef struct _view_t
{
tf_http_request_t* request;
const char** form_data;
void* data;
size_t size;
char etag[256];
char notify_want_blob_id[k_blob_id_len];
bool not_modified;
} view_t;
static bool _is_filename_safe(const char* filename)
{
if (!filename)
{
return NULL;
}
for (const char* p = filename; *p; p++)
{
if ((*p <= 'a' && *p >= 'z') && (*p <= 'A' && *p >= 'Z') && (*p <= '0' && *p >= '9') && *p != '.' && *p != '-' && *p != '_')
{
return false;
}
}
return strlen(filename) < 256;
}
static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data)
{
view_t* view = user_data;
tf_http_request_t* request = view->request;
char blob_id[k_blob_id_len] = "";
tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/view");
if (user_app)
{
size_t app_path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(app_path_length);
snprintf(app_path, app_path_length, "path:%s", user_app->app);
const char* value = tf_ssb_db_get_property(ssb, user_app->user, app_path);
tf_string_set(blob_id, sizeof(blob_id), value);
tf_free(app_path);
tf_free((void*)value);
}
else if (request->path[0] == '/' && request->path[1] == '&')
{
snprintf(blob_id, sizeof(blob_id), "%.*s", (int)(strlen(request->path) - strlen("/view") - 1), request->path + 1);
}
tf_free(user_app);
if (*blob_id)
{
snprintf(view->etag, sizeof(view->etag), "\"%s\"", blob_id);
const char* if_none_match = tf_http_request_get_header(request, "if-none-match");
char match[258];
snprintf(match, sizeof(match), "\"%s\"", blob_id);
if (if_none_match && strcmp(if_none_match, match) == 0)
{
view->not_modified = true;
}
else
{
if (!tf_ssb_db_blob_get(ssb, blob_id, (uint8_t**)&view->data, &view->size))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
tf_ssb_db_add_blob_wants(db, blob_id);
tf_ssb_release_db_writer(ssb, db);
tf_string_set(view->notify_want_blob_id, sizeof(view->notify_want_blob_id), blob_id);
}
}
}
}
static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
view_t* view = user_data;
const char* filename = tf_httpd_form_data_get(view->form_data, "filename");
if (!_is_filename_safe(filename))
{
filename = NULL;
}
char content_disposition[512] = "";
if (filename)
{
snprintf(content_disposition, sizeof(content_disposition), "attachment; filename=%s", filename);
}
const char* headers[] = {
"Content-Security-Policy",
"sandbox allow-downloads allow-top-navigation-by-user-activation",
"Content-Type",
view->data ? tf_httpd_magic_bytes_to_content_type(view->data, view->size) : "text/plain",
"etag",
view->etag,
filename ? "Content-Disposition" : NULL,
filename ? content_disposition : NULL,
};
int count = filename ? tf_countof(headers) / 2 : (tf_countof(headers) / 2 - 1);
if (view->not_modified)
{
tf_http_respond(view->request, 304, headers, count, NULL, 0);
}
else if (view->data)
{
tf_http_respond(view->request, 200, headers, count, view->data, view->size);
tf_free(view->data);
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(view->request, 404, NULL, 0, k_payload, strlen(k_payload));
}
if (*view->notify_want_blob_id)
{
tf_ssb_notify_blob_want_added(ssb, view->notify_want_blob_id);
}
tf_free(view->form_data);
tf_http_request_unref(view->request);
tf_free(view);
}
void tf_httpd_endpoint_view(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
view_t* view = tf_malloc(sizeof(view_t));
*view = (view_t) { .request = request, .form_data = tf_httpd_form_data_decode(request->query, request->query ? strlen(request->query) : 0) };
tf_ssb_run_work(ssb, _httpd_endpoint_view_work, _httpd_endpoint_view_after_work, view);
}

View File

@@ -13,13 +13,13 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.31</string>
<string>0.2025.8</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>13</string>
<string>16</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>LSRequiresIPhoneOS</key>

View File

@@ -169,8 +169,8 @@ typedef struct _command_t
const command_t k_commands[] = {
{ "run", _tf_command_run, "Run tildefriends (default)." },
{ "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." },
{ "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." },
{ "import", _tf_command_import, "Import apps from file to the database." },
{ "export", _tf_command_export, "Export apps from the database to file." },
{ "publish", _tf_command_publish, "Append a message to a feed." },
{ "private", _tf_command_private, "Append a private post message to a feed." },
{ "create_invite", _tf_command_create_invite, "Create an invite." },
@@ -185,6 +185,18 @@ const command_t k_commands[] = {
{ "test", _tf_command_test, "Test SSB." },
};
static const char* _description(const char* name)
{
for (int i = 0; i < tf_countof(k_commands); i++)
{
if (strcmp(name, k_commands[i].name) == 0)
{
return k_commands[i].description;
}
}
return NULL;
}
static int _tf_command_test(const char* file, int argc, char* argv[])
{
#if !defined(__ANDROID__)
@@ -233,8 +245,9 @@ static int _tf_command_test(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s test [options]\n\n", file);
tf_printf("options\n");
tf_printf("\nUsage: %s test [options]\n\n", file);
tf_printf("%s\n\n", _description("test"));
tf_printf("options:\n");
tf_printf(" -t, --tests tests Comma-separated list of tests to run. (default: all)\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
@@ -288,7 +301,8 @@ static int _tf_command_import(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("\nUsage: %s import [options] [paths...]\n\n", file);
tf_printf("%s\n\n", _description("import"));
tf_printf("options:\n");
tf_printf(" -u, --user user User into whose account apps will be imported (default: \"import\").\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
@@ -357,7 +371,8 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s export [options] [paths...]\n\n", file);
tf_printf("\nUsage: %s export [options] [paths...]\n\n", file);
tf_printf("%s\n\n", _description("export"));
tf_printf("options:\n");
tf_printf(" -u, --user user User from whose account apps will be exported (default: \"core\").\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
@@ -473,7 +488,8 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
if (show_usage || !identity || !content)
{
tf_printf("\n%s publish [options]\n\n", file);
tf_printf("\nUsage: %s publish [options]\n\n", file);
tf_printf("%s\n\n", _description("publish"));
tf_printf("options:\n");
tf_printf(" -u, --user user User owning identity with which to publish.\n");
tf_printf(" -i, --id identity Identity with which to publish message.\n");
@@ -501,7 +517,7 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
{
JSContext* context = tf_ssb_get_context(ssb);
int64_t sequence = 0;
int32_t sequence = 0;
char previous[k_id_base64_len] = { 0 };
tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, previous, sizeof(previous));
JSValue content_value = JS_ParseJSON(context, content, strlen(content), NULL);
@@ -592,7 +608,8 @@ static int _tf_command_private(const char* file, int argc, char* argv[])
if (show_usage || !identity || !recipients || !text)
{
tf_printf("\n%s private [options]\n\n", file);
tf_printf("\nUsage: %s private [options]\n\n", file);
tf_printf("%s\n\n", _description("private"));
tf_printf("options:\n");
tf_printf(" -u, --user user User owning identity with which to publish (optional).\n");
tf_printf(" -i, --id identity Identity with which to publish message.\n");
@@ -653,7 +670,7 @@ static int _tf_command_private(const char* file, int argc, char* argv[])
char* encrypted = tf_ssb_private_message_encrypt(private_key, recipient_list, recipient_count, message_str, strlen(message_str));
if (encrypted)
{
int64_t sequence = 0;
int32_t sequence = 0;
char previous[k_id_base64_len] = { 0 };
tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, previous, sizeof(previous));
@@ -723,7 +740,8 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
if (show_usage || !file_path)
{
tf_printf("\n%s store_blob [options]\n\n", file);
tf_printf("\nUsage: %s store_blob [options]\n\n", file);
tf_printf("%s\n\n", _description("store_blob"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -f, --file file_path Path to file to add to the blob store.\n");
@@ -825,7 +843,8 @@ static int _tf_command_get_blob(const char* file, int argc, char* argv[])
if (show_usage || !blob_id)
{
tf_printf("\n%s store_blob [options]\n\n", file);
tf_printf("\nUsage: %s get_blob [options]\n\n", file);
tf_printf("%s\n\n", _description("get_blob"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -b, --blob blob_id Blob identifier to retrieve.\n");
@@ -927,7 +946,8 @@ static int _tf_command_has_blob(const char* file, int argc, char* argv[])
if (show_usage || !blob_id)
{
tf_printf("\n%s has_blob [options]\n\n", file);
tf_printf("\nUsage: %s has_blob [options]\n\n", file);
tf_printf("%s\n\n", _description("has_blob"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -b, --blob_id blob_id ID of blob to query.\n");
@@ -1005,7 +1025,8 @@ static int _tf_command_create_invite(const char* file, int argc, char* argv[])
if (show_usage || !identity || !use_count || !expires || !host || !port)
{
tf_printf("\n%s get_sequence [options]\n\n", file);
tf_printf("\nUsage: %s create_invite [options]\n\n", file);
tf_printf("%s\n\n", _description("create_invite"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
@@ -1073,7 +1094,8 @@ static int _tf_command_get_sequence(const char* file, int argc, char* argv[])
if (show_usage || !identity)
{
tf_printf("\n%s get_sequence [options]\n\n", file);
tf_printf("\nUsage: %s get_sequence [options]\n\n", file);
tf_printf("%s\n\n", _description("get_sequence"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
@@ -1084,9 +1106,9 @@ static int _tf_command_get_sequence(const char* file, int argc, char* argv[])
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
tf_ssb_set_quiet(ssb, true);
int64_t sequence = -1;
int32_t sequence = -1;
int result = tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, NULL, 0) ? EXIT_SUCCESS : EXIT_FAILURE;
tf_printf("%" PRId64 "\n", sequence);
tf_printf("%d\n", sequence);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
@@ -1126,7 +1148,8 @@ static int _tf_command_get_identity(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s get_identity [options]\n\n", file);
tf_printf("\nUsage: %s get_identity [options]\n\n", file);
tf_printf("%s\n\n", _description("get_identity"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -h, --help Show this usage information.\n");
@@ -1183,7 +1206,8 @@ static int _tf_command_get_profile(const char* file, int argc, char* argv[])
if (show_usage || !identity)
{
tf_printf("\n%s get_profile [options]\n\n", file);
tf_printf("\nUsage: %s get_profile [options]\n\n", file);
tf_printf("%s\n\n", _description("get_profile"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account for which to get profile information.\n");
@@ -1242,7 +1266,8 @@ static int _tf_command_get_contacts(const char* file, int argc, char* argv[])
if (show_usage || !identity)
{
tf_printf("\n%s get_contacts [options]\n\n", file);
tf_printf("\nUsage: %s get_contacts [options]\n\n", file);
tf_printf("%s\n\n", _description("get_contacts"));
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get contact information.\n");
@@ -1304,7 +1329,7 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
const char* identity = NULL;
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
int64_t sequence = 0;
int32_t sequence = 0;
bool show_usage = false;
while (!show_usage)
@@ -1333,7 +1358,7 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
identity = optarg;
break;
case 's':
sequence = atoll(optarg);
sequence = atoi(optarg);
break;
case 'd':
db_path = optarg;
@@ -1343,7 +1368,8 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("\nUsage: %s verify [options]\n\n", file);
tf_printf("%s\n\n", _description("verify"));
tf_printf("options:\n");
tf_printf(" -i, --identity identity Identity to verify.\n");
tf_printf(" -s, --sequence sequence Sequence number to debug.\n");
@@ -1416,7 +1442,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
int result = -1;
tf_task_t* task = tf_task_create();
tf_task_set_trusted(task, true);
tf_printf("setting zip path to %s\n", args->zip);
tf_printf("Zip path: %s\n", args->zip);
tf_task_set_zip_path(task, args->zip);
tf_task_set_ssb_network_key(task, args->network_key);
tf_task_set_args(task, args->args);
@@ -1461,7 +1487,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
break;
}
}
tf_printf("Using %s as the working directory.\n", cwd);
tf_printf("Root path: %s\n", cwd);
}
if (!*cwd)
{
@@ -1671,8 +1697,11 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\n%s run [options]\n\n", file);
tf_printf("options\n");
tf_printf("\nUsage: %s run [options]\n\n", file);
#if !defined(__ANDROID__)
tf_printf("%s\n\n", _description("run"));
#endif
tf_printf("options:\n");
tf_printf(" -s, --script script Script to run (default: core/core.js).\n");
tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -k, --ssb-network-key key SSB network key to use.\n");
@@ -1757,6 +1786,9 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
if (show_usage)
{
tf_printf("\nUsage: %s sandbox [options]\n\n", file);
#if !defined(__ANDROID__)
tf_printf("%s\n\n", _description("sandbox"));
#endif
tf_printf("options:\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_printf(" -f, --fd File descriptor with which to communicate with parent process.\n");
@@ -2026,6 +2058,7 @@ void tf_run_thread_start(const char* zip_path)
#else
int main(int argc, char* argv[])
{
setvbuf(stdout, NULL, _IONBF, 0);
_startup(argc, argv);
ares_library_init(0);

View File

@@ -3,6 +3,7 @@
#include "util.js.h"
#include "quickjs.h"
#include "sodium/crypto_generichash.h"
#include "sqlite3.h"
#include "uv.h"
@@ -154,11 +155,11 @@ static void _tf_mem_summarize(void* ptr, size_t size, int frames_count, void* co
{
summary_t* summary = user_data;
tf_mem_allocation_t allocation = {
.stack_hash = tf_util_fnv32a(frames, sizeof(void*) * frames_count, 0),
.count = 1,
.size = size,
.frames_count = frames_count,
};
crypto_generichash((void*)&allocation.stack_hash, sizeof(allocation.stack_hash), (const void*)frames, sizeof(void*) * frames_count, NULL, 0);
memcpy(allocation.frames, frames, sizeof(void*) * frames_count);
int index = tf_util_insert_index(&allocation, summary->allocations, summary->count, sizeof(tf_mem_allocation_t), _tf_mem_hash_stack_compare);

View File

@@ -489,7 +489,7 @@ static JSValue _socket_connect(JSContext* context, JSValueConst this_val, int ar
const char* node = JS_ToCString(context, argv[0]);
const char* port = JS_ToCString(context, argv[1]);
snprintf(socket->_peerName, sizeof(socket->_peerName), "%s", node);
tf_string_set(socket->_peerName, sizeof(socket->_peerName), node);
socket_resolve_data_t* data = tf_malloc(sizeof(socket_resolve_data_t));
memset(data, 0, sizeof(*data));
@@ -1062,9 +1062,7 @@ static void _socket_onShutdown(uv_shutdown_t* request, int status)
}
else
{
char error[256];
snprintf(error, sizeof(error), "uv_shutdown: %s", uv_strerror(status));
tf_task_reject_promise(socket->_task, promise, JS_ThrowInternalError(tf_task_get_context(socket->_task), "%s", error));
tf_task_reject_promise(socket->_task, promise, JS_ThrowInternalError(tf_task_get_context(socket->_task), "uv_shutdown: %s", uv_strerror(status)));
}
tf_free(request);
}

173
src/ssb.c
View File

@@ -34,9 +34,6 @@
#define CYAN "\e[1;36m"
#define RESET "\e[0m"
#define PRE_CALLBACK(ssb, cb) uint64_t pre_callback_hrtime_ns = _tf_ssb_callback_pre(ssb)
#define POST_CALLBACK(ssb, cb) _tf_ssb_callback_post(ssb, cb, pre_callback_hrtime_ns)
const int k_read_back_pressure_threshold = 256;
static_assert(k_id_base64_len == sodium_base64_ENCODED_LEN(9 + crypto_box_PUBLICKEYBYTES, sodium_base64_VARIANT_ORIGINAL), "k_id_base64_len");
@@ -168,6 +165,12 @@ typedef struct _tf_ssb_timer_t
void* user_data;
} tf_ssb_timer_t;
typedef struct _tf_ssb_broadcast_result_t
{
struct sockaddr_storage addr;
int result;
} tf_ssb_broadcast_result_t;
typedef struct _tf_ssb_t
{
bool own_context;
@@ -183,6 +186,7 @@ typedef struct _tf_ssb_t
sqlite3* db_writer;
sqlite3** db_readers;
int db_readers_count;
int db_ref_count;
uv_loop_t own_loop;
uv_loop_t* loop;
@@ -194,6 +198,9 @@ typedef struct _tf_ssb_t
uv_timer_t request_activity_timer;
uv_tcp_t server;
tf_ssb_broadcast_result_t* broadcast_results;
int broadcast_results_count;
uint8_t network_key[32];
uint8_t pub[crypto_sign_PUBLICKEYBYTES];
@@ -237,9 +244,6 @@ typedef struct _tf_ssb_t
int32_t thread_busy_count;
int32_t thread_busy_max;
void (*hitch_callback)(const char* name, uint64_t duration, void* user_data);
void* hitch_user_data;
tf_ssb_store_queue_t store_queue;
int ref_count;
@@ -368,8 +372,6 @@ static int s_connection_index;
static int s_tunnel_index;
static void _tf_ssb_add_broadcast(tf_ssb_t* ssb, const tf_ssb_broadcast_t* broadcast, int expires_seconds);
static uint64_t _tf_ssb_callback_pre(tf_ssb_t* ssb);
static void _tf_ssb_callback_post(tf_ssb_t* ssb, void* callback, uint64_t pre);
static void _tf_ssb_connection_client_send_hello(tf_ssb_connection_t* connection);
static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const char* reason);
static void _tf_ssb_connection_finalizer(JSRuntime* runtime, JSValue value);
@@ -645,9 +647,7 @@ static void _tf_ssb_connection_dispatch_scheduled(tf_ssb_connection_t* connectio
connection->scheduled_count--;
tf_trace_begin(connection->ssb->trace, "scheduled callback");
PRE_CALLBACK(connection->ssb, scheduled.callback);
scheduled.callback(connection, false, scheduled.user_data);
POST_CALLBACK(connection->ssb, scheduled.callback);
tf_trace_end(connection->ssb->trace);
}
}
@@ -666,9 +666,7 @@ void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, const char
{
/* Skip the new request. */
tf_trace_begin(connection->ssb->trace, "scheduled callback (skip)");
PRE_CALLBACK(connection->ssb, callback);
callback(connection, true, user_data);
POST_CALLBACK(connection->ssb, callback);
tf_trace_end(connection->ssb->trace);
}
else
@@ -679,7 +677,7 @@ void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, const char
.callback = callback,
.user_data = user_data,
};
snprintf(connection->scheduled[index].key, sizeof(connection->scheduled[index].key), "%s", key);
tf_string_set(connection->scheduled[index].key, sizeof(connection->scheduled[index].key), key);
connection->scheduled_count++;
uv_async_send(&connection->scheduled_async);
@@ -800,7 +798,7 @@ void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t requ
.dependent_connection = dependent_connection,
.last_active = now_ms,
};
snprintf(request.name, sizeof(request.name), "%s", name);
tf_string_set(request.name, sizeof(request.name), name);
int index = tf_util_insert_index(&request_number, connection->requests, connection->requests_count, sizeof(tf_ssb_request_t), _request_compare);
connection->requests = tf_resize_vec(connection->requests, sizeof(tf_ssb_request_t) * (connection->requests_count + 1));
if (connection->requests_count - index)
@@ -853,7 +851,7 @@ void tf_ssb_connection_add_new_message_request(tf_ssb_connection_t* connection,
.request_number = request_number,
.keys = keys,
};
snprintf(connection->message_requests[index].author, sizeof(connection->message_requests[index].author), "%s", author);
tf_string_set(connection->message_requests[index].author, sizeof(connection->message_requests[index].author), author);
connection->message_requests_count++;
}
@@ -979,7 +977,6 @@ bool tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* co
{
char buffer[1024] = "";
snprintf(buffer, sizeof(buffer), "method '%s' is not in list of allowed methods", name);
tf_printf("%s\n", buffer);
return tf_ssb_connection_rpc_send_error(connection, flags, request_number, buffer);
}
@@ -1249,22 +1246,6 @@ bool tf_ssb_id_str_to_bin(uint8_t* bin, const char* str)
return author_id && type ? tf_base64_decode(author_id, type - author_id, bin, crypto_box_PUBLICKEYBYTES) != 0 : false;
}
static uint64_t _tf_ssb_callback_pre(tf_ssb_t* ssb)
{
return uv_hrtime();
}
static void _tf_ssb_callback_post(tf_ssb_t* ssb, void* callback, uint64_t pre)
{
if (ssb->hitch_callback)
{
uint64_t post = uv_hrtime();
const char* name = tf_util_function_to_string(callback);
ssb->hitch_callback(name, post - pre, ssb->hitch_user_data);
tf_free((void*)name);
}
}
static void _tf_ssb_notify_connections_changed(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection)
{
tf_ssb_connections_changed_callback_node_t* next = NULL;
@@ -1272,9 +1253,7 @@ static void _tf_ssb_notify_connections_changed(tf_ssb_t* ssb, tf_ssb_change_t ch
{
next = node->next;
tf_trace_begin(ssb->trace, "connections_changed");
PRE_CALLBACK(ssb, node->callback);
node->callback(ssb, change, connection, node->user_data);
POST_CALLBACK(ssb, node->callback);
tf_trace_end(ssb->trace);
}
}
@@ -1383,9 +1362,7 @@ static void _tf_ssb_connection_verify_identity(tf_ssb_connection_t* connection,
connection->state = k_tf_ssb_state_verified;
if (connection->connect_callback)
{
PRE_CALLBACK(connection->ssb, connection->connect_callback);
connection->connect_callback(connection, NULL, connection->connect_callback_user_data);
POST_CALLBACK(connection->ssb, connection->connect_callback);
connection->connect_callback = NULL;
connection->connect_callback_user_data = NULL;
}
@@ -1734,7 +1711,7 @@ static void _tf_ssb_name_to_string(JSContext* context, JSValue object, char* buf
else if (JS_IsString(name))
{
const char* part_str = JS_ToCString(context, name);
snprintf(buffer, size, "%s", part_str);
tf_string_set(buffer, size, part_str);
JS_FreeCString(context, part_str);
}
JS_FreeValue(context, name);
@@ -1777,9 +1754,7 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
char buffer[64];
snprintf(buffer, sizeof(buffer), "request %s:%d", request_name, request_number);
tf_trace_begin(connection->ssb->trace, buffer);
PRE_CALLBACK(connection->ssb, callback);
callback(connection, flags, request_number, val, message, size, user_data);
POST_CALLBACK(connection->ssb, callback);
tf_trace_end(connection->ssb->trace);
if (!(flags & k_ssb_rpc_flag_stream))
{
@@ -1798,9 +1773,7 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
{
tf_ssb_connection_add_request(connection, -request_number, name, NULL, NULL, NULL, NULL);
tf_trace_begin(connection->ssb->trace, it->name);
PRE_CALLBACK(connection->ssb, it->callback);
it->callback(connection, flags, request_number, val, message, size, it->user_data);
POST_CALLBACK(connection->ssb, it->callback);
tf_trace_end(connection->ssb->trace);
found = true;
break;
@@ -1840,9 +1813,7 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
char buffer[64];
snprintf(buffer, sizeof(buffer), "request %s:%d", request_name, request_number);
tf_trace_begin(connection->ssb->trace, buffer);
PRE_CALLBACK(connection->ssb, callback);
callback(connection, flags, request_number, JS_UNDEFINED, message, size, user_data);
POST_CALLBACK(connection->ssb, callback);
tf_trace_end(connection->ssb->trace);
}
}
@@ -1876,7 +1847,7 @@ static void _tf_ssb_connection_rpc_recv_push(tf_ssb_connection_t* connection, co
while (connection->rpc_recv_size >= 9)
{
uint8_t flags = *connection->rpc_recv_buffer;
uint8_t flags = (*connection->rpc_recv_buffer) & 0xf;
uint32_t body_len;
int32_t request_number;
memcpy(&body_len, connection->rpc_recv_buffer + 1, sizeof(body_len));
@@ -1954,15 +1925,15 @@ static bool _tf_ssb_connection_box_stream_recv(tf_ssb_connection_t* connection)
return true;
}
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int64_t previous_sequence)
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int32_t previous_sequence)
{
char actual_previous_id[crypto_hash_sha256_BYTES * 2];
int64_t actual_previous_sequence = 0;
int32_t actual_previous_sequence = 0;
bool have_previous = false;
if (previous_id)
{
have_previous = *previous_id && previous_sequence > 0;
snprintf(actual_previous_id, sizeof(actual_previous_id), "%s", previous_id);
tf_string_set(actual_previous_id, sizeof(actual_previous_id), previous_id);
actual_previous_sequence = previous_sequence;
}
else
@@ -1975,7 +1946,7 @@ JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* pr
JS_SetPropertyStr(context, root, "previous", have_previous ? JS_NewString(context, actual_previous_id) : JS_NULL);
JS_SetPropertyStr(context, root, "author", JS_NewString(context, author));
JS_SetPropertyStr(context, root, "sequence", JS_NewInt64(context, actual_previous_sequence + 1));
JS_SetPropertyStr(context, root, "sequence", JS_NewInt32(context, actual_previous_sequence + 1));
int64_t now = (int64_t)time(NULL);
JS_SetPropertyStr(context, root, "timestamp", JS_NewInt64(context, now * 1000LL));
@@ -2029,9 +2000,7 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
}
if (connection->connect_callback)
{
PRE_CALLBACK(connection->ssb, connection->connect_callback);
connection->connect_callback(NULL, reason, connection->connect_callback_user_data);
POST_CALLBACK(connection->ssb, connection->connect_callback);
connection->connect_callback = NULL;
connection->connect_callback_user_data = NULL;
}
@@ -2519,6 +2488,7 @@ sqlite3* tf_ssb_acquire_db_reader(tf_ssb_t* ssb)
tf_ssb_db_init_reader(db);
}
tf_trace_sqlite(ssb->trace, db);
ssb->db_ref_count++;
uv_mutex_unlock(&ssb->db_readers_lock);
sqlite3_set_authorizer(db, NULL, NULL);
return db;
@@ -2535,10 +2505,17 @@ void tf_ssb_release_db_reader(tf_ssb_t* ssb, sqlite3* db)
{
sqlite3_db_release_memory(db);
uv_mutex_lock(&ssb->db_readers_lock);
ssb->db_ref_count--;
bool destroy = ssb->shutting_down_deferred && ssb->db_ref_count == 0;
ssb->db_readers = tf_resize_vec(ssb->db_readers, sizeof(sqlite3*) * (ssb->db_readers_count + 1));
ssb->db_readers[ssb->db_readers_count++] = db;
uv_mutex_unlock(&ssb->db_readers_lock);
tf_trace_end(ssb->trace);
if (destroy)
{
tf_ssb_destroy(ssb);
}
}
sqlite3* tf_ssb_acquire_db_writer(tf_ssb_t* ssb)
@@ -2832,15 +2809,6 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
JS_FreeRuntime(ssb->runtime);
ssb->own_context = false;
}
if (ssb->db_writer)
{
int r = sqlite3_close(ssb->db_writer);
if (r != SQLITE_OK)
{
tf_printf("sqlite3_close: %s\n", sqlite3_errstr(r));
}
ssb->db_writer = NULL;
}
while (ssb->broadcasts)
{
tf_ssb_broadcast_t* broadcast = ssb->broadcasts;
@@ -2856,6 +2824,15 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_printf("sqlite3_close: %s\n", sqlite3_errstr(r));
}
}
if (ssb->db_writer)
{
int r = sqlite3_close(ssb->db_writer);
if (r != SQLITE_OK)
{
tf_printf("sqlite3_close: %s\n", sqlite3_errstr(r));
}
ssb->db_writer = NULL;
}
ssb->db_readers_count = 0;
if (ssb->db_readers)
{
@@ -2872,9 +2849,15 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_free(ssb->room_name);
ssb->room_name = NULL;
}
if (ssb->broadcast_results_count)
{
tf_free(ssb->broadcast_results);
ssb->broadcast_results = NULL;
ssb->broadcast_results_count = 0;
}
ssb->shutting_down_deferred = true;
if (ssb->connection_ref_count == 0)
if (ssb->connection_ref_count == 0 && ssb->db_ref_count == 0)
{
uv_mutex_destroy(&ssb->db_readers_lock);
uv_mutex_destroy(&ssb->db_writer_lock);
@@ -2996,7 +2979,7 @@ static tf_ssb_connection_t* _tf_ssb_connection_create(
tf_ssb_connection_t* connection = _tf_ssb_connection_create_internal(ssb, "cli", s_connection_index++);
connection->connect.data = connection;
snprintf(connection->host, sizeof(connection->host), "%s", host);
tf_string_set(connection->host, sizeof(connection->host), host);
connection->port = ntohs(addr->sin_port);
connection->connect_callback = callback;
connection->connect_callback_user_data = user_data;
@@ -3171,7 +3154,7 @@ void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* ke
{
tf_ssb_connections_store(ssb->connections_tracker, host, port, id);
}
snprintf(connect->host, sizeof(connect->host), "%s", host);
tf_string_set(connect->host, sizeof(connect->host), host);
memcpy(connect->key, key, k_id_bin_len);
tf_ssb_ref(ssb);
int r = uv_getaddrinfo(ssb->loop, &connect->req, _tf_on_connect_getaddrinfo, host, NULL, &(struct addrinfo) { .ai_family = AF_INET });
@@ -3220,7 +3203,7 @@ static void _tf_ssb_connect_with_invite(
{
tf_ssb_connections_store(ssb->connections_tracker, host, port, id);
}
snprintf(connect->host, sizeof(connect->host), "%s", host);
tf_string_set(connect->host, sizeof(connect->host), host);
memcpy(connect->key, key, k_id_bin_len);
memcpy(connect->invite, invite, sizeof(connect->invite));
tf_ssb_ref(ssb);
@@ -3288,6 +3271,39 @@ static void _tf_ssb_on_connection(uv_stream_t* stream, int status)
_tf_ssb_connection_read_start(connection);
}
static void _tf_ssb_update_broadcast_result(tf_ssb_t* ssb, struct sockaddr* address, const char* address_str, int result)
{
for (int i = 0; i < ssb->broadcast_results_count; i++)
{
if (ssb->broadcast_results[i].addr.ss_family == address->sa_family && address->sa_family == AF_INET &&
memcmp(&ssb->broadcast_results[i].addr, address, sizeof(struct sockaddr_in)) == 0)
{
if (result != ssb->broadcast_results[i].result)
{
if (result)
{
char broadcast_str[256] = { 0 };
uv_ip4_name((struct sockaddr_in*)address, broadcast_str, sizeof(broadcast_str));
tf_printf("Unable to send broadcast for %s via %s (%d): %s.\n", address_str, broadcast_str, result, uv_strerror(result));
}
ssb->broadcast_results[i].result = result;
}
return;
}
}
if (address->sa_family == AF_INET)
{
struct sockaddr_storage storage = { 0 };
memcpy(&storage, address, sizeof(struct sockaddr_in));
ssb->broadcast_results = tf_resize_vec(ssb->broadcast_results, sizeof(tf_ssb_broadcast_result_t) * (ssb->broadcast_results_count + 1));
ssb->broadcast_results[ssb->broadcast_results_count++] = (tf_ssb_broadcast_result_t) {
.result = result,
.addr = storage,
};
}
}
static void _tf_ssb_send_broadcast(tf_ssb_t* ssb, struct sockaddr_in* address, struct sockaddr_in* netmask)
{
struct sockaddr server_addr;
@@ -3321,12 +3337,7 @@ static void _tf_ssb_send_broadcast(tf_ssb_t* ssb, struct sockaddr_in* address, s
broadcast_addr.sin_port = htons(8008);
broadcast_addr.sin_addr.s_addr = (address->sin_addr.s_addr & netmask->sin_addr.s_addr) | (INADDR_BROADCAST & ~netmask->sin_addr.s_addr);
r = uv_udp_try_send(&ssb->broadcast_sender, &buf, 1, (struct sockaddr*)&broadcast_addr);
if (r < 0)
{
char broadcast_str[256] = { 0 };
uv_ip4_name(&broadcast_addr, broadcast_str, sizeof(broadcast_str));
tf_printf("failed to send broadcast for %s via %s (%d): %s\n", address_str, broadcast_str, r, uv_strerror(r));
}
_tf_ssb_update_broadcast_result(ssb, (struct sockaddr*)&broadcast_addr, address_str, r);
}
typedef struct _seeds_t
@@ -3551,9 +3562,7 @@ static void _tf_ssb_notify_broadcasts_changed(tf_ssb_t* ssb)
if (node->callback)
{
tf_trace_begin(ssb->trace, "broadcasts changed");
PRE_CALLBACK(ssb, node->callback);
node->callback(ssb, node->user_data);
POST_CALLBACK(ssb, node->callback);
tf_trace_end(ssb->trace);
}
}
@@ -3657,9 +3666,7 @@ void tf_ssb_visit_broadcasts(tf_ssb_t* ssb,
if (node->mtime - now < 60)
{
tf_trace_begin(ssb->trace, "broadcast");
PRE_CALLBACK(ssb, callback);
callback(node->host, &node->addr, node->origin, node->tunnel_connection, node->pub, user_data);
POST_CALLBACK(ssb, callback);
tf_trace_end(ssb->trace);
}
}
@@ -3958,7 +3965,7 @@ void tf_ssb_notify_blob_stored(tf_ssb_t* ssb, const char* id)
ssb->blobs_stored++;
}
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int64_t sequence, const char* id, JSValue message_keys)
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_keys)
{
tf_ssb_message_added_callback_node_t* next = NULL;
ssb->messages_stored++;
@@ -3966,9 +3973,7 @@ void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int64_t sequ
{
next = node->next;
tf_trace_begin(ssb->trace, "message added callback");
PRE_CALLBACK(ssb, node->callback);
node->callback(ssb, author, sequence, id, node->user_data);
POST_CALLBACK(ssb, node->callback);
tf_trace_end(ssb->trace);
}
@@ -4044,9 +4049,7 @@ void tf_ssb_notify_blob_want_added(tf_ssb_t* ssb, const char* id)
{
next = node->next;
tf_trace_begin(ssb->trace, "blob want added callback");
PRE_CALLBACK(ssb, node->callback);
node->callback(ssb, id, node->user_data);
POST_CALLBACK(ssb, node->callback);
tf_trace_end(ssb->trace);
}
}
@@ -4231,12 +4234,6 @@ float tf_ssb_get_average_thread_percent(tf_ssb_t* ssb)
return 100.0f * ssb->thread_busy_count / ssb->thread_busy_max;
}
void tf_ssb_set_hitch_callback(tf_ssb_t* ssb, void (*callback)(const char* name, uint64_t duration_ns, void* user_data), void* user_data)
{
ssb->hitch_callback = callback;
ssb->hitch_user_data = user_data;
}
tf_ssb_store_queue_t* tf_ssb_get_store_queue(tf_ssb_t* ssb)
{
return &ssb->store_queue;
@@ -4292,9 +4289,7 @@ static void _tf_ssb_connection_after_work_callback(uv_work_t* work, int status)
if (data->after_work_callback)
{
tf_trace_begin(data->connection->ssb->trace, data->after_name);
PRE_CALLBACK(data->connection->ssb, data->after_work_callback);
data->after_work_callback(data->connection, status, data->user_data);
POST_CALLBACK(data->connection->ssb, data->after_work_callback);
tf_trace_end(data->connection->ssb->trace);
}
data->connection->ref_count--;
@@ -4361,9 +4356,7 @@ static void _tf_ssb_after_work_callback(uv_work_t* work, int status)
if (data->after_work_callback)
{
tf_trace_begin(data->ssb->trace, data->after_name);
PRE_CALLBACK(data->ssb, data->after_work_callback);
data->after_work_callback(data->ssb, status, data->user_data);
POST_CALLBACK(data->ssb, data->after_work_callback);
tf_trace_end(data->ssb->trace);
}
tf_ssb_unref(data->ssb);
@@ -4485,7 +4478,7 @@ static void _tf_ssb_update_settings_after_work(tf_ssb_t* ssb, int result, void*
uv_timer_start(&ssb->broadcast_timer, _tf_ssb_broadcast_timer, 2000, 2000);
}
ssb->discovery = update->discovery;
snprintf(ssb->seeds_host, sizeof(ssb->seeds_host), "%s", update->seeds_host);
tf_string_set(ssb->seeds_host, sizeof(ssb->seeds_host), update->seeds_host);
_tf_ssb_start_update_settings(ssb);
tf_free(update);
}
@@ -4526,9 +4519,7 @@ void tf_ssb_set_quiet(tf_ssb_t* ssb, bool quiet)
static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
{
tf_ssb_timer_t* timer = handle->data;
PRE_CALLBACK(timer->ssb, timer->callback);
timer->callback(timer->ssb, timer->user_data);
POST_CALLBACK(timer->ssb, timer->callback);
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
}

View File

@@ -2,6 +2,7 @@
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "util.js.h"
@@ -51,19 +52,23 @@ static void _tf_ssb_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t
}
}
static bool _tf_ssb_connections_get_next_connection(tf_ssb_connections_t* connections, char* host, size_t host_size, int* port, char* key, size_t key_size)
static bool _tf_ssb_connections_get_next_connection(
tf_ssb_connections_t* connections, char* host, size_t host_size, int* port, char* key, size_t key_size, bool* out_stay_connected)
{
bool result = false;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(connections->ssb);
tf_ssb_db_get_global_setting_bool(db, "stay_connected", out_stay_connected);
if (sqlite3_prepare_v2(db, "SELECT host, port, key FROM connections WHERE last_attempt IS NULL OR (strftime('%s', 'now') - last_attempt > ?1) ORDER BY last_attempt LIMIT 1",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_int(statement, 1, 60000) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
if (sqlite3_bind_int(statement, 1, *out_stay_connected ? 15 : 60000) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
snprintf(host, host_size, "%s", sqlite3_column_text(statement, 0));
tf_string_set(host, host_size, (const char*)sqlite3_column_text(statement, 0));
*port = sqlite3_column_int(statement, 1);
snprintf(key, key_size, "%s", sqlite3_column_text(statement, 2));
tf_string_set(key, key_size, (const char*)sqlite3_column_text(statement, 2));
result = true;
}
sqlite3_finalize(statement);
@@ -80,6 +85,8 @@ typedef struct _tf_ssb_connections_get_next_t
{
tf_ssb_connections_t* connections;
bool ready;
bool stay_connected;
bool full;
char host[256];
int port;
char key[k_id_base64_len];
@@ -92,7 +99,7 @@ static void _tf_ssb_connections_get_next_work(tf_ssb_t* ssb, void* user_data)
{
return;
}
next->ready = _tf_ssb_connections_get_next_connection(next->connections, next->host, sizeof(next->host), &next->port, next->key, sizeof(next->key));
next->ready = _tf_ssb_connections_get_next_connection(next->connections, next->host, sizeof(next->host), &next->port, next->key, sizeof(next->key), &next->stay_connected);
}
static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, void* user_data)
@@ -100,12 +107,20 @@ static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, v
tf_ssb_connections_get_next_t* next = user_data;
if (next->ready)
{
/*
** Might be a duplicate connection or otherwise discarded by
** tf_ssb_connect() before we otherwise set attempted, so do it
** here.
*/
tf_ssb_connections_set_attempted(next->connections, next->host, next->port, next->key);
uint8_t key_bin[k_id_bin_len];
if (tf_ssb_id_str_to_bin(key_bin, next->key))
{
tf_ssb_connect(ssb, next->host, next->port, key_bin, k_tf_ssb_connect_flag_do_not_store, NULL, NULL);
}
}
uv_timer_set_repeat(&next->connections->timer, next->stay_connected ? (next->full ? 2000 : 200) : (next->full ? 10000 : 2000));
tf_free(next);
}
@@ -124,6 +139,7 @@ static void _tf_ssb_connections_timer(uv_timer_t* timer)
tf_ssb_connections_get_next_t* next = tf_malloc(sizeof(tf_ssb_connections_get_next_t));
*next = (tf_ssb_connections_get_next_t) {
.connections = connections,
.full = count + 1 == tf_countof(active),
};
tf_ssb_run_work(connections->ssb, _tf_ssb_connections_get_next_work, _tf_ssb_connections_get_next_after_work, next);
}
@@ -246,8 +262,8 @@ void tf_ssb_connections_store(tf_ssb_connections_t* connections, const char* hos
*update = (tf_ssb_connections_update_t) {
.port = port,
};
snprintf(update->host, sizeof(update->host), "%s", host);
snprintf(update->key, sizeof(update->key), "%s", key);
tf_string_set(update->host, sizeof(update->host), host);
tf_string_set(update->key, sizeof(update->key), key);
_tf_ssb_connections_queue_update(connections, update);
}
@@ -258,8 +274,8 @@ void tf_ssb_connections_set_attempted(tf_ssb_connections_t* connections, const c
.port = port,
.attempted = true,
};
snprintf(update->host, sizeof(update->host), "%s", host);
snprintf(update->key, sizeof(update->key), "%s", key);
tf_string_set(update->host, sizeof(update->host), host);
tf_string_set(update->key, sizeof(update->key), key);
_tf_ssb_connections_queue_update(connections, update);
}
@@ -270,8 +286,8 @@ void tf_ssb_connections_set_succeeded(tf_ssb_connections_t* connections, const c
.port = port,
.succeeded = true,
};
snprintf(update->host, sizeof(update->host), "%s", host);
snprintf(update->key, sizeof(update->key), "%s", key);
tf_string_set(update->host, sizeof(update->host), host);
tf_string_set(update->key, sizeof(update->key), key);
_tf_ssb_connections_queue_update(connections, update);
}

View File

@@ -31,6 +31,10 @@ static int _tf_ssb_db_try_exec(sqlite3* db, const char* statement)
{
tf_printf("Error running '%s': %s.\n", statement, error ? error : sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
return result;
}
@@ -41,6 +45,13 @@ static void _tf_ssb_db_exec(sqlite3* db, const char* statement)
if (result != SQLITE_OK)
{
tf_printf("Error running '%s': %s.\n", statement, error ? error : sqlite3_errmsg(db));
}
if (error)
{
sqlite3_free(error);
}
if (result != SQLITE_OK)
{
abort();
}
}
@@ -76,6 +87,26 @@ static int _tf_ssb_db_busy_handler(void* user_data, int count)
return 1;
}
static int _tf_ssb_db_wal_hook(void* user_data, sqlite3* db, const char* db_name, int log_pages)
{
/* Keeps the log below about 64MB with default 4096 byte pages. */
if (log_pages >= 16384)
{
int log = 0;
int checkpointed = 0;
uint64_t checkpoint_start_ns = uv_hrtime();
if (sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, &log, &checkpointed) == SQLITE_OK)
{
tf_printf("Checkpointed %d pages in %d ms. Log is now %d frames.\n", log_pages, (int)((uv_hrtime() - checkpoint_start_ns) / 1000000LL), log);
}
else
{
tf_printf("Checkpoint: %s.\n", sqlite3_errmsg(db));
}
}
return SQLITE_OK;
}
static void _tf_ssb_db_init_internal(sqlite3* db)
{
sqlite3_extended_result_codes(db, 1);
@@ -93,6 +124,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
_tf_ssb_db_init_internal(db);
sqlite3_wal_hook(db, _tf_ssb_db_wal_hook, NULL);
sqlite3_stmt* statement = NULL;
int auto_vacuum = 0;
@@ -200,6 +232,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_size_by_author_index ON messages (author, length(content))");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_type_root_index ON messages (author, content ->> 'type', content ->> 'root')");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_author_id_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_by_author_index");
_tf_ssb_db_exec(db, "DROP INDEX IF EXISTS messages_id_index");
@@ -277,6 +310,13 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.rowid, "
"old.content); END");
if (_tf_ssb_db_has_rows(db, "SELECT * FROM sqlite_schema WHERE type = 'trigger' AND name = 'messages_ai_refs' AND NOT sql LIKE '%ltrim%'"))
{
tf_printf("Deleting incorrect messages_refs...\n");
_tf_ssb_db_exec(db, "DROP TABLE IF EXISTS messages_refs");
tf_printf("Done.\n");
}
if (!_tf_ssb_db_has_rows(db, "PRAGMA table_list('messages_refs')"))
{
_tf_ssb_db_exec(db, "BEGIN TRANSACTION");
@@ -291,8 +331,9 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"INSERT INTO messages_refs(message, ref) "
"SELECT messages.id, j.value FROM messages, json_tree(messages.content) AS j WHERE "
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '%%%.sha256' OR "
"j.value LIKE '@%.ed25519' "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"ON CONFLICT DO NOTHING");
_tf_ssb_db_exec(db, "COMMIT TRANSACTION");
tf_printf("Done.\n");
@@ -304,8 +345,9 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
"INSERT INTO messages_refs(message, ref) "
"SELECT DISTINCT new.id, j.value FROM json_tree(new.content) AS j WHERE "
"j.value LIKE '&%.sha256' OR "
"j.value LIKE '%%%.sha256' OR "
"j.value LIKE '@%.ed25519' "
"j.value LIKE '!%%.sha256' ESCAPE '!' OR "
"j.value LIKE '@%.ed25519' OR "
"(j.value LIKE '#%' AND ltrim(substr(j.value, 2), 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_') = '') "
"ON CONFLICT DO NOTHING; END");
_tf_ssb_db_exec(db, "DROP TRIGGER IF EXISTS messages_ad_refs");
_tf_ssb_db_exec(db, "CREATE TRIGGER IF NOT EXISTS messages_ad_refs AFTER DELETE ON messages BEGIN DELETE FROM messages_refs WHERE messages_refs.message = old.id; END");
@@ -454,7 +496,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
tf_ssb_release_db_writer(ssb, db);
}
static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int64_t sequence, const char* previous, bool* out_id_mismatch)
static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author, int32_t sequence, const char* previous, bool* out_id_mismatch)
{
bool exists = false;
if (sequence == 1)
@@ -466,7 +508,7 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(db, "SELECT COUNT(*), id != ?3 AS is_mismatch FROM messages WHERE author = ?1 AND sequence = ?2", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, sequence - 1) == SQLITE_OK &&
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, sequence - 1) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, previous, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
exists = sqlite3_column_int(statement, 0) != 0;
@@ -478,7 +520,7 @@ static bool _tf_ssb_db_previous_message_exists(sqlite3* db, const char* author,
return exists;
}
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int64_t sequence, double timestamp, const char* content,
static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const char* previous, const char* author, int32_t sequence, double timestamp, const char* content,
size_t content_len, const char* signature, int flags)
{
int64_t last_row_id = -1;
@@ -493,7 +535,7 @@ static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const c
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK &&
(previous ? sqlite3_bind_text(statement, 2, previous, -1, NULL) : sqlite3_bind_null(statement, 2)) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 4, sequence) == SQLITE_OK &&
sqlite3_bind_double(statement, 5, timestamp) == SQLITE_OK && sqlite3_bind_text(statement, 6, content, content_len, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 7, "sha256", 6, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 8, signature, -1, NULL) == SQLITE_OK &&
sqlite3_bind_int(statement, 9, flags) == SQLITE_OK)
@@ -527,7 +569,7 @@ static int64_t _tf_ssb_db_store_message_raw(sqlite3* db, const char* id, const c
** message when trying to receive what we don't have, and
** that's not helping anybody.
*/
tf_printf("%p: Previous message doesn't exist for author=%s sequence=%" PRId64 " previous=%s.\n", db, author, sequence, previous);
tf_printf("%p: Previous message doesn't exist for author=%s sequence=%d previous=%s.\n", db, author, sequence, previous);
}
return last_row_id;
}
@@ -541,7 +583,7 @@ static char* _tf_ssb_db_get_message_blob_wants(tf_ssb_t* ssb, int64_t rowid)
if (sqlite3_prepare_v2(db,
"SELECT DISTINCT json.value FROM messages, json_tree(messages.content) AS json LEFT OUTER JOIN blobs ON json.value = blobs.id WHERE messages.rowid = ?1 AND "
"json.value LIKE '&%%.sha256' AND length(json.value) = ?2 AND blobs.content IS NULL",
"json.value LIKE '&%.sha256' AND length(json.value) = ?2 AND blobs.content IS NULL",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_int64(statement, 1, rowid) == SQLITE_OK && sqlite3_bind_int(statement, 2, k_blob_id_len - 1) == SQLITE_OK)
@@ -584,7 +626,7 @@ typedef struct _message_store_t
int flags;
char previous[k_id_base64_len];
char author[k_id_base64_len];
int64_t sequence;
int32_t sequence;
double timestamp;
const char* content;
size_t length;
@@ -720,9 +762,9 @@ void tf_ssb_db_store_message(
const char* author = JS_ToCString(context, authorval);
JS_FreeValue(context, authorval);
int64_t sequence = -1;
int32_t sequence = -1;
JSValue sequenceval = JS_GetPropertyStr(context, val, "sequence");
JS_ToInt64(context, &sequence, sequenceval);
JS_ToInt32(context, &sequence, sequenceval);
JS_FreeValue(context, sequenceval);
double timestamp = -1.0;
@@ -748,10 +790,10 @@ void tf_ssb_db_store_message(
.callback = callback,
.user_data = user_data,
};
snprintf(store->id, sizeof(store->id), "%s", id);
snprintf(store->previous, sizeof(store->previous), "%s", previous ? previous : "");
snprintf(store->author, sizeof(store->author), "%s", author);
snprintf(store->signature, sizeof(store->signature), "%s", signature);
tf_string_set(store->id, sizeof(store->id), id);
tf_string_set(store->previous, sizeof(store->previous), previous ? previous : "");
tf_string_set(store->author, sizeof(store->author), author);
tf_string_set(store->signature, sizeof(store->signature), signature);
JS_FreeCString(context, author);
JS_FreeCString(context, previous);
@@ -908,7 +950,7 @@ void tf_ssb_db_blob_get_async(tf_ssb_t* ssb, const char* id, tf_ssb_db_blob_get_
.callback = callback,
.user_data = user_data,
};
snprintf(async->id, sizeof(async->id), "%s", id);
tf_string_set(async->id, sizeof(async->id), id);
tf_ssb_run_work(ssb, _tf_ssb_db_blob_get_async_work, _tf_ssb_db_blob_get_async_after_work, async);
}
@@ -1005,7 +1047,7 @@ bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char*
if (result && out_id)
{
snprintf(out_id, out_id_size, "%s", id);
tf_string_set(out_id, out_id_size, id);
}
if (out_new)
{
@@ -1014,7 +1056,7 @@ bool tf_ssb_db_blob_store(tf_ssb_t* ssb, const uint8_t* blob, size_t size, char*
return result;
}
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int32_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
size_t out_previous_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, size_t out_signature_size, int* out_flags)
{
bool found = false;
@@ -1023,11 +1065,11 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int64(statement, 2, sequence) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
if (sqlite3_bind_text(statement, 1, author, -1, NULL) == SQLITE_OK && sqlite3_bind_int(statement, 2, sequence) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
if (out_message_id)
{
snprintf(out_message_id, out_message_id_size, "%s", (const char*)sqlite3_column_text(statement, 0));
tf_string_set(out_message_id, out_message_id_size, (const char*)sqlite3_column_text(statement, 0));
}
if (out_previous)
{
@@ -1040,7 +1082,7 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
}
else
{
snprintf(out_previous, out_previous_size, "%s", (const char*)sqlite3_column_text(statement, 1));
tf_string_set(out_previous, out_previous_size, (const char*)sqlite3_column_text(statement, 1));
}
}
if (out_timestamp)
@@ -1053,11 +1095,11 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
}
if (out_hash)
{
snprintf(out_hash, out_hash_size, "%s", (const char*)sqlite3_column_text(statement, 4));
tf_string_set(out_hash, out_hash_size, (const char*)sqlite3_column_text(statement, 4));
}
if (out_signature)
{
snprintf(out_signature, out_signature_size, "%s", (const char*)sqlite3_column_text(statement, 5));
tf_string_set(out_signature, out_signature_size, (const char*)sqlite3_column_text(statement, 5));
}
if (out_flags)
{
@@ -1075,7 +1117,7 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
return found;
}
bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, int64_t* out_sequence, char* out_message_id, size_t out_message_id_size)
bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, int32_t* out_sequence, char* out_message_id, size_t out_message_id_size)
{
bool found = false;
sqlite3_stmt* statement;
@@ -1090,7 +1132,7 @@ bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, i
{
if (out_sequence)
{
*out_sequence = sqlite3_column_int64(statement, 1);
*out_sequence = sqlite3_column_int(statement, 1);
}
if (out_message_id)
{
@@ -1114,7 +1156,7 @@ bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, i
{
if (out_sequence)
{
*out_sequence = sqlite3_column_int64(statement, 0);
*out_sequence = sqlite3_column_int(statement, 0);
}
found = true;
}
@@ -1292,19 +1334,19 @@ JSValue tf_ssb_db_visit_query(tf_ssb_t* ssb, const char* query, const JSValue bi
}
JSValue tf_ssb_format_message(
JSContext* context, const char* previous, const char* author, int64_t sequence, double timestamp, const char* hash, const char* content, const char* signature, int flags)
JSContext* context, const char* previous, const char* author, int32_t sequence, double timestamp, const char* hash, const char* content, const char* signature, int flags)
{
JSValue value = JS_NewObject(context);
JS_SetPropertyStr(context, value, "previous", (previous && *previous) ? JS_NewString(context, previous) : JS_NULL);
if (flags & k_tf_ssb_message_flag_sequence_before_author)
{
JS_SetPropertyStr(context, value, "sequence", JS_NewInt64(context, sequence));
JS_SetPropertyStr(context, value, "sequence", JS_NewInt32(context, sequence));
JS_SetPropertyStr(context, value, "author", JS_NewString(context, author));
}
else
{
JS_SetPropertyStr(context, value, "author", JS_NewString(context, author));
JS_SetPropertyStr(context, value, "sequence", JS_NewInt64(context, sequence));
JS_SetPropertyStr(context, value, "sequence", JS_NewInt32(context, sequence));
}
JS_SetPropertyStr(context, value, "timestamp", JS_NewFloat64(context, timestamp));
JS_SetPropertyStr(context, value, "hash", JS_NewString(context, hash));
@@ -1511,14 +1553,14 @@ typedef struct _following_t following_t;
typedef struct _following_t
{
char id[k_id_base64_len];
bool populated;
int depth;
following_t** following;
following_t** blocking;
int following_count;
int blocking_count;
int depth;
int ref_count;
int block_ref_count;
bool populated;
} following_t;
static int _following_compare(const void* a, const void* b)
@@ -1610,7 +1652,7 @@ static following_t* _make_following_node(const char* id, following_t*** followin
(*following_count)++;
memset(entry, 0, sizeof(*entry));
entry->depth = INT_MAX;
snprintf(entry->id, sizeof(entry->id), "%s", id);
tf_string_set(entry->id, sizeof(entry->id), id);
}
return entry;
}
@@ -1704,7 +1746,7 @@ static sqlite3_stmt* _make_following_statement(sqlite3* db)
"SELECT content ->> '$.contact' AS contact, content ->> '$.following', content ->> '$.blocking' "
"FROM messages "
"WHERE author = ? AND content ->> '$.type' = 'contact' AND contact IS NOT NULL "
"ORDER BY content ->> '$.contact', sequence",
"ORDER BY sequence",
-1, &statement, NULL) != SQLITE_OK)
{
tf_printf("prepare failed: %s", sqlite3_errmsg(db));
@@ -1737,22 +1779,23 @@ tf_ssb_following_t* tf_ssb_db_following_deep(tf_ssb_t* ssb, const char** ids, in
}
tf_ssb_following_t* result = tf_malloc(sizeof(tf_ssb_following_t) * (actual_following_count + 1));
memset(result, 0, sizeof(tf_ssb_following_t) * (actual_following_count + 1));
int write_index = 0;
for (int i = 0; i < following_count; i++)
{
if (following[i]->ref_count > 0 || include_blocks)
{
snprintf(result[write_index].id, sizeof(result[write_index].id), "%s", following[i]->id);
result[write_index].following_count = following[i]->following_count;
result[write_index].blocking_count = following[i]->blocking_count;
result[write_index].followed_by_count = following[i]->ref_count;
result[write_index].blocked_by_count = following[i]->block_ref_count;
result[write_index].depth = following[i]->depth;
result[write_index] = (tf_ssb_following_t) {
.following_count = following[i]->following_count,
.blocking_count = following[i]->blocking_count,
.followed_by_count = following[i]->ref_count,
.blocked_by_count = following[i]->block_ref_count,
.depth = following[i]->depth,
};
tf_string_set(result[write_index].id, sizeof(result[write_index].id), following[i]->id);
write_index++;
}
}
result[write_index] = (tf_ssb_following_t) { 0 };
for (int i = 0; i < following_count; i++)
{
@@ -1799,7 +1842,7 @@ const char** tf_ssb_db_following_deep_ids(tf_ssb_t* ssb, const char** ids, int c
if (following[i]->ref_count > 0)
{
result[write_index] = result_ids + k_id_base64_len * write_index;
snprintf(result[write_index], k_id_base64_len, "%s", following[i]->id);
tf_string_set(result[write_index], k_id_base64_len, following[i]->id);
write_index++;
}
}
@@ -1859,7 +1902,7 @@ JSValue tf_ssb_db_get_message_by_id(tf_ssb_t* ssb, const char* id, bool is_keys)
{
JSValue message = JS_UNDEFINED;
JSValue formatted = tf_ssb_format_message(context, (const char*)sqlite3_column_text(statement, 0), (const char*)sqlite3_column_text(statement, 1),
sqlite3_column_int64(statement, 3), sqlite3_column_double(statement, 4), (const char*)sqlite3_column_text(statement, 5),
sqlite3_column_int(statement, 3), sqlite3_column_double(statement, 4), (const char*)sqlite3_column_text(statement, 5),
(const char*)sqlite3_column_text(statement, 6), (const char*)sqlite3_column_text(statement, 7), sqlite3_column_int(statement, 8));
if (is_keys)
{
@@ -1888,16 +1931,18 @@ tf_ssb_db_stored_connection_t* tf_ssb_db_get_stored_connections(tf_ssb_t* ssb, i
int count = 0;
sqlite3_stmt* statement;
if (sqlite3_prepare_v2(db, "SELECT host, port, key FROM connections ORDER BY host, port, key", -1, &statement, NULL) == SQLITE_OK)
if (sqlite3_prepare_v2(db, "SELECT host, port, key, last_attempt, last_success FROM connections ORDER BY host, port, key", -1, &statement, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
result = tf_resize_vec(result, sizeof(tf_ssb_db_stored_connection_t) * (count + 1));
result[count] = (tf_ssb_db_stored_connection_t) {
.port = sqlite3_column_int(statement, 1),
.last_attempt = sqlite3_column_int64(statement, 3),
.last_success = sqlite3_column_int64(statement, 4),
};
snprintf(result[count].address, sizeof(result[count].address), "%s", (const char*)sqlite3_column_text(statement, 0));
snprintf(result[count].pubkey, sizeof(result[count].pubkey), "%s", (const char*)sqlite3_column_text(statement, 2));
tf_string_set(result[count].address, sizeof(result[count].address), (const char*)sqlite3_column_text(statement, 0));
tf_string_set(result[count].pubkey, sizeof(result[count].pubkey), (const char*)sqlite3_column_text(statement, 2));
count++;
}
sqlite3_finalize(statement);
@@ -1935,7 +1980,7 @@ bool tf_ssb_db_get_account_password_hash(tf_ssb_t* ssb, const char* name, char*
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
snprintf(out_password, password_size, "%s", (const char*)sqlite3_column_text(statement, 0));
tf_string_set(out_password, password_size, (const char*)sqlite3_column_text(statement, 0));
result = true;
}
}
@@ -2133,7 +2178,7 @@ bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* pa
if (sqlite3_bind_text(statement, 1, user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, package_owner, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, package_name, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW)
{
snprintf(out_identity, out_identity_size, "%s", (const char*)sqlite3_column_text(statement, 0));
tf_string_set(out_identity, out_identity_size, (const char*)sqlite3_column_text(statement, 0));
found = true;
}
sqlite3_finalize(statement);
@@ -2245,14 +2290,14 @@ static void _tf_ssb_db_set_flags(tf_ssb_t* ssb, const char* message_id, int flag
tf_ssb_release_db_writer(ssb, db);
}
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int64_t debug_sequence, bool fix)
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int32_t debug_sequence, bool fix)
{
JSContext* context = tf_ssb_get_context(ssb);
bool verified = true;
int64_t sequence = -1;
int32_t sequence = -1;
if (tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0))
{
for (int64_t i = 1; i <= sequence; i++)
for (int32_t i = 1; i <= sequence; i++)
{
char message_id[k_id_base64_len];
char previous[256];
@@ -2271,12 +2316,12 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int64_t debug_sequence, boo
if (!tf_ssb_verify_and_strip_signature(context, message, i == debug_sequence ? k_tf_ssb_verify_flag_debug : 0, calculated_id, sizeof(calculated_id),
extracted_signature, sizeof(extracted_signature), &calculated_flags))
{
tf_printf("author=%s sequence=%" PRId64 " verify failed.\n", id, i);
tf_printf("author=%s sequence=%d verify failed.\n", id, i);
verified = false;
}
if (calculated_flags != flags)
{
tf_printf("author=%s sequence=%" PRId64 " flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
tf_printf("author=%s sequence=%d flag mismatch %d => %d.\n", id, i, flags, calculated_flags);
if (fix)
{
_tf_ssb_db_set_flags(ssb, message_id, calculated_flags);
@@ -2288,7 +2333,7 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int64_t debug_sequence, boo
}
if (strcmp(message_id, calculated_id))
{
tf_printf("author=%s sequence=%" PRId64 " id mismatch %s => %s.\n", id, i, message_id, calculated_id);
tf_printf("author=%s sequence=%d id mismatch %s => %s.\n", id, i, message_id, calculated_id);
verified = false;
}
JS_FreeValue(context, message);
@@ -2301,7 +2346,7 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int64_t debug_sequence, boo
}
else
{
tf_printf("Unable to find message with sequence=%" PRId64 " for author=%s.", i, id);
tf_printf("Unable to find message with sequence=%d for author=%s.", i, id);
verified = false;
break;
}
@@ -2403,7 +2448,7 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
snprintf(out_value, size, "%s", sqlite3_column_text(statement, 0));
tf_string_set(out_value, size, (const char*)sqlite3_column_text(statement, 0));
result = true;
}
}
@@ -2415,7 +2460,7 @@ bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* ou
}
if (!result)
{
snprintf(out_value, size, "%s", tf_util_get_default_global_setting_string(name));
tf_string_set(out_value, size, tf_util_get_default_global_setting_string(name));
}
return result;
}
@@ -2697,7 +2742,7 @@ tf_ssb_identity_info_t* tf_ssb_db_get_identity_info(tf_ssb_t* ssb, const char* u
tf_ssb_db_identity_get_active(db, user, package_owner, package_name, info->active_identity, sizeof(info->active_identity));
if (!*info->active_identity && info->count)
{
snprintf(info->active_identity, sizeof(info->active_identity), "%s", info->identity[0]);
tf_string_set(info->active_identity, sizeof(info->active_identity), info->identity[0]);
}
tf_ssb_release_db_reader(ssb, db);

View File

@@ -151,7 +151,7 @@ JSValue tf_ssb_db_get_message_by_id(tf_ssb_t* ssb, const char* id, bool is_keys)
** @param[out] out_flags Populated with flags describing the format of the message.
** @return True if the message was found and retrieved.
*/
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int64_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* author, int32_t sequence, char* out_message_id, size_t out_message_id_size, char* out_previous,
size_t out_previous_size, double* out_timestamp, char** out_content, char* out_hash, size_t out_hash_size, char* out_signature, size_t out_signature_size, int* out_flags);
/**
@@ -163,7 +163,7 @@ bool tf_ssb_db_get_message_by_author_and_sequence(tf_ssb_t* ssb, const char* aut
** @param out_message_id_size The size of the out_message_id buffer.
** @return True if the message was found and information was retrieved.
*/
bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, int64_t* out_sequence, char* out_message_id, size_t out_message_id_size);
bool tf_ssb_db_get_latest_message_by_author(tf_ssb_t* ssb, const char* author, int32_t* out_sequence, char* out_message_id, size_t out_message_id_size);
/**
** Call a function for each result row of an SQL query.
@@ -281,7 +281,7 @@ bool tf_ssb_db_identity_get_private_key(tf_ssb_t* ssb, const char* user, const c
** @param flags tf_ssb_message_flags_t describing the message.
*/
JSValue tf_ssb_format_message(
JSContext* context, const char* previous, const char* author, int64_t sequence, double timestamp, const char* hash, const char* content, const char* signature, int flags);
JSContext* context, const char* previous, const char* author, int32_t sequence, double timestamp, const char* hash, const char* content, const char* signature, int flags);
/** Information about a single followed account. */
typedef struct _tf_ssb_following_t
@@ -340,6 +340,10 @@ typedef struct _tf_ssb_db_stored_connection_t
int port;
/** The identity. */
char pubkey[k_id_base64_len];
/** Time of last attempted connection. */
int64_t last_attempt;
/** Time of last successful connection. */
int64_t last_success;
} tf_ssb_db_stored_connection_t;
/**
@@ -456,7 +460,7 @@ void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callb
** @param fix Fix invalid messages when possible.
** @return true If the feed verified successfully.
*/
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int64_t debug_sequence, bool fix);
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id, int32_t debug_sequence, bool fix);
/**
** Check if a user has a specific permission.

View File

@@ -102,7 +102,7 @@ static ebt_entry_t* _ebt_get_entry(tf_ssb_ebt_t* ebt, const char* id)
.in = -1,
.out = -1,
};
snprintf(ebt->entries[index].id, sizeof(ebt->entries[index].id), "%s", id);
tf_string_set(ebt->entries[index].id, sizeof(ebt->entries[index].id), id);
ebt->entries_count++;
return &ebt->entries[index];
}
@@ -127,10 +127,9 @@ void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clo
}
if (!JS_IsUndefined(in_clock))
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
const char* author = JS_ToCString(context, key);
int64_t sequence = -1;
JS_ToInt64(context, &sequence, in_clock);
const char* author = JS_AtomToCString(context, ptab[i].atom);
int32_t sequence = -1;
JS_ToInt32(context, &sequence, in_clock);
ebt_entry_t* entry = _ebt_get_entry(ebt, author);
if (entry)
@@ -153,7 +152,6 @@ void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clo
}
}
JS_FreeCString(context, author);
JS_FreeValue(context, key);
}
JS_FreeValue(context, in_clock);
}
@@ -212,7 +210,7 @@ static void _ebt_add_to_clock(ebt_get_clock_t* work, const char* id, int64_t val
memmove(work->clock->entries + index + 1, work->clock->entries + index, (count - index) * sizeof(tf_ssb_ebt_clock_entry_t));
}
work->clock->entries[index] = (tf_ssb_ebt_clock_entry_t) { .value = out_value };
snprintf(work->clock->entries[index].id, sizeof(work->clock->entries[index].id), "%s", id);
tf_string_set(work->clock->entries[index].id, sizeof(work->clock->entries[index].id), id);
work->clock->count = count + 1;
}
}
@@ -237,12 +235,12 @@ static void _tf_ssb_ebt_get_send_clock_work(tf_ssb_connection_t* connection, voi
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
if (visible)
{
int64_t* sequences = NULL;
int32_t* sequences = NULL;
for (int i = 0; visible[i]; i++)
{
int64_t sequence = 0;
int32_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, visible[i], &sequence, NULL, 0);
sequences = tf_resize_vec(sequences, (i + 1) * sizeof(int64_t));
sequences = tf_resize_vec(sequences, (i + 1) * sizeof(int32_t));
sequences[i] = sequence;
}
@@ -261,7 +259,7 @@ static void _tf_ssb_ebt_get_send_clock_work(tf_ssb_connection_t* connection, voi
char id[k_id_base64_len] = "";
if (tf_ssb_connection_get_id(connection, id, sizeof(id)))
{
int64_t sequence = 0;
int32_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
uv_mutex_lock(&work->ebt->mutex);
_ebt_add_to_clock(work, id, sequence, true, true);
@@ -279,7 +277,7 @@ static void _tf_ssb_ebt_get_send_clock_work(tf_ssb_connection_t* connection, voi
{
requested = tf_resize_vec(requested, (requested_count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
requested[requested_count] = (tf_ssb_ebt_clock_entry_t) { .value = -1 };
snprintf(requested[requested_count].id, sizeof(requested[requested_count].id), "%s", entry->id);
tf_string_set(requested[requested_count].id, sizeof(requested[requested_count].id), entry->id);
requested_count++;
}
}
@@ -342,7 +340,7 @@ tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt)
{
clock = tf_resize_vec(clock, sizeof(tf_ssb_ebt_clock_t) + (count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
clock->entries[count] = (tf_ssb_ebt_clock_entry_t) { .value = entry->in };
snprintf(clock->entries[count].id, sizeof(clock->entries[count].id), "%s", entry->id);
tf_string_set(clock->entries[count].id, sizeof(clock->entries[count].id), entry->id);
clock->count = ++count;
}
}
@@ -350,7 +348,7 @@ tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt)
return clock;
}
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence)
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int32_t sequence)
{
uv_mutex_lock(&ebt->mutex);
ebt_entry_t* entry = _ebt_get_entry(ebt, id);
@@ -365,7 +363,7 @@ void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t seq
uv_mutex_unlock(&ebt->mutex);
}
void tf_ssb_ebt_set_messages_received(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence)
void tf_ssb_ebt_set_messages_received(tf_ssb_ebt_t* ebt, const char* id, int32_t sequence)
{
uv_mutex_lock(&ebt->mutex);
ebt_entry_t* entry = _ebt_get_entry(ebt, id);
@@ -386,28 +384,6 @@ void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending)
ebt->send_clock_pending = pending;
}
void tf_ssb_ebt_debug_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue debug)
{
uv_mutex_lock(&ebt->mutex);
for (int i = 0; i < ebt->entries_count; i++)
{
ebt_entry_t* entry = &ebt->entries[i];
JSValue clock = JS_NewObject(context);
JSValue out = JS_NewObject(context);
JSValue in = JS_NewObject(context);
JS_SetPropertyStr(context, out, "value", JS_NewInt64(context, entry->out));
JS_SetPropertyStr(context, out, "replicate", JS_NewBool(context, entry->out_replicate));
JS_SetPropertyStr(context, out, "receive", JS_NewBool(context, entry->out_receive));
JS_SetPropertyStr(context, clock, "out", out);
JS_SetPropertyStr(context, in, "value", JS_NewInt64(context, entry->in));
JS_SetPropertyStr(context, in, "replicate", JS_NewBool(context, entry->in_replicate));
JS_SetPropertyStr(context, in, "receive", JS_NewBool(context, entry->in_receive));
JS_SetPropertyStr(context, clock, "in", in);
JS_SetPropertyStr(context, debug, entry->id, clock);
}
uv_mutex_unlock(&ebt->mutex);
}
void tf_ssb_ebt_get_progress(tf_ssb_ebt_t* ebt, int* in_pending, int* in_total, int* out_pending, int* out_total)
{
uv_mutex_lock(&ebt->mutex);

View File

@@ -19,7 +19,7 @@ typedef struct _tf_ssb_ebt_clock_entry_t
/** The identity. */
char id[k_id_base64_len];
/** The sequence number. */
int64_t value;
int32_t value;
} tf_ssb_ebt_clock_entry_t;
/**
@@ -76,7 +76,7 @@ tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt);
** @param id The identity to update.
** @param sequence The maximum sequence number sent.
*/
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence);
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int32_t sequence);
/**
** Update the clock state indicating the messages that have been received for an account.
@@ -84,7 +84,7 @@ void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t seq
** @param id The identity to update.
** @param sequence The maximum sequence number received.
*/
void tf_ssb_ebt_set_messages_received(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence);
void tf_ssb_ebt_set_messages_received(tf_ssb_ebt_t* ebt, const char* id, int32_t sequence);
/**
** Destroy an EBT instance.
@@ -106,15 +106,6 @@ int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt);
*/
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending);
/**
** Get a JSON representation of the clock state for
** debugging.
** @param ebt The EBT instance.
** @param context The JS context.
** @param debug A JS object populated with the information.
*/
void tf_ssb_ebt_debug_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue debug);
/**
** Get a representation of sync progress.
** @param ebt The EBT instance.

View File

@@ -142,8 +142,7 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
JSPropertyDescriptor desc;
if (JS_GetOwnProperty(context, &desc, files, ptab[i].atom) == 1)
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
const char* file_name = JS_ToCString(context, key);
const char* file_name = JS_AtomToCString(context, ptab[i].atom);
const char* blob_id = JS_ToCString(context, desc.value);
uint8_t* file_blob = NULL;
@@ -156,7 +155,6 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
}
JS_FreeCString(context, file_name);
JS_FreeValue(context, key);
JS_FreeCString(context, blob_id);
JS_FreeValue(context, desc.value);
JS_FreeValue(context, desc.setter);

View File

@@ -323,7 +323,7 @@ void tf_ssb_run(tf_ssb_t* ssb);
** @param previous_sequence The sequence number of the previous message in the feed. Optional.
** @return The signed message.
*/
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int64_t previous_sequence);
JSValue tf_ssb_sign_message(tf_ssb_t* ssb, const char* author, const uint8_t* private_key, JSValue message, const char* previous_id, int32_t previous_sequence);
/**
** Get the server's identity.
@@ -651,7 +651,7 @@ void tf_ssb_remove_broadcasts_changed_callback(tf_ssb_t* ssb, tf_ssb_broadcasts_
** @param id The message identifier.
** @param user_data The user data.
*/
typedef void(tf_ssb_message_added_callback_t)(tf_ssb_t* ssb, const char* author, int64_t sequence, const char* id, void* user_data);
typedef void(tf_ssb_message_added_callback_t)(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, void* user_data);
/**
** Register a callback called when a message is added to the database.
@@ -678,7 +678,7 @@ void tf_ssb_remove_message_added_callback(tf_ssb_t* ssb, tf_ssb_message_added_ca
** @param id The message identity added.
** @param message_with_keys The message added in the format required if keys are requested.
*/
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int64_t sequence, const char* id, JSValue message_with_keys);
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_with_keys);
/**
** Record that a new blob was stored.
@@ -1006,15 +1006,6 @@ void tf_ssb_record_thread_busy(tf_ssb_t* ssb, bool busy);
*/
float tf_ssb_get_average_thread_percent(tf_ssb_t* ssb);
/**
** Register a callback to be called when the main thread blocks for an
** unreasonable amount of time.
** @param ssb The SSB instance.
** @param callback The callback to call.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_set_hitch_callback(tf_ssb_t* ssb, void (*callback)(const char* name, uint64_t duration_ns, void* user_data), void* user_data);
/**
** Get the queue of messages in the progress of being stored.
** @param ssb The SSB instance.

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