Compare commits

...

164 Commits

Author SHA1 Message Date
ca28b5ca82 Delete some code that doesn't need to exist.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4319 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-01 22:53:44 +00:00
19e26c1759 Support setting publicWebHosting, and kill some unused code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4318 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-06-01 22:21:14 +00:00
790f6643a4 Mostly fumbling with error handling.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4317 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-27 16:51:56 +00:00
2158ad3c0b sqlStream => sqlAsync
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4316 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-24 00:10:05 +00:00
d904d8922f Oops, no verbose.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4315 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:36:21 +00:00
da50792500 Avoid chunked content encoding. Some WebViewClient debugging. Doesn't go to a blank screen on android so much.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4314 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:26:07 +00:00
b4629acc48 Ugg. libuv and io_uring and android: https://github.com/libuv/libuv/issues/4010.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4313 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 23:06:59 +00:00
0cf4118330 Remove Socket.info.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4312 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:47:25 +00:00
dd61a6ecc3 Report which method was not found.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4311 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:16:07 +00:00
8e6f1284e1 Show the edit pane before it finishes loading so that it's more clear it's working.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4310 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-23 22:03:17 +00:00
813d3cd492 Lit Element 2.7.4.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4309 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-21 21:46:32 +00:00
f421606e21 libuv 1.45.0, #include cleanup, probably something else.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4308 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-21 21:36:51 +00:00
1ccb9183b4 Don't mess with websockets when we're returning a document from an app's handler.js.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4307 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-19 19:57:40 +00:00
7d9b627f37 Report attempts to call tfrpc methods that aren't registered.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4306 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-19 19:47:33 +00:00
3038138909 This is a sketch of a setup that allows apps to produce sandboxed dynamic content without all the iframe/websocket business.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4305 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 20:22:13 +00:00
2ca08d21e4 I broke magic byte detection, and missed some Content-Security-Policy opportunities.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4304 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:57:56 +00:00
478e96fc5f Just moving HTTP code around.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4303 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:35:58 +00:00
e237c7ea1d Remove valgrind hooks. In this house, we use asan and custom allocators. Smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4302 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 18:24:10 +00:00
bf9ff088fd Handle unsuccessfully decrypted messages, too.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4301 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 16:57:43 +00:00
e073ebedd1 sqlite-amalgamation-3420000.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4300 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 14:15:29 +00:00
10d4ae7dcc Decrypt messages in the ssb app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4299 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-17 14:10:49 +00:00
5b8bdbb3e4 Today I discovered the "Content-Security-Policy: sandbox" header.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4298 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 19:46:01 +00:00
c807e21c6b Don't let browsers render untrusted HTML or SVG outside of the iframe. Do let them fetch JS and such.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4297 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 19:31:45 +00:00
cc92d0e316 Simplify magic bytes lookup slightly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4296 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:47:19 +00:00
09c396d5a3 Default the files panel to expanded.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4295 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:05:28 +00:00
bc5bbca951 Remove importing and export from the ssb app. I like it better as the separate sneaker app.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4294 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-14 18:02:56 +00:00
ed4faedcd7 Report some information when importing messages and discover an old verification bug.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4293 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-11 00:22:42 +00:00
251556ebed Sneakernet, here we come.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4292 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 23:52:46 +00:00
1324afb459 Zip export still had stringified content.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4291 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:52:34 +00:00
1119804fc2 Whitespace.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4290 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:47:58 +00:00
cdf6440197 Uncommit unintended part of previous change.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4289 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:47:20 +00:00
8727fe00af Return something from ssb.storeMessage.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4288 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:45:37 +00:00
7da7890bb6 Work in progress zip import/export.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4287 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-10 01:30:15 +00:00
706bd2c51f Save some space + more deterministic with relative paths for debug info.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4286 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:44:32 +00:00
acabec940e Make emojis.json much smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4285 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:32:50 +00:00
470b998b61 Add a vector launcher icon. Currently the smiling face with sunglasses emoji from openmoji.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4284 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-04 00:04:43 +00:00
80fad05f23 Show latest value on the spark line graphs.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4283 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 23:37:02 +00:00
07a912fb9a Files pane => lit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4282 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 23:12:34 +00:00
e9d83262c4 Sparkline graph tweaks. Minor cleanup.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4281 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 22:47:00 +00:00
74323c22f9 I think this lets me load more pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4280 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-03 22:32:21 +00:00
2614e89b1b Actually update to lit 2.7.3.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4279 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-02 16:50:26 +00:00
e092fe1399 Updated lit, starting to improve the display of mentions during editing, . to refresh, and probably some other things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4278 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-05-02 16:47:27 +00:00
9cbe895cb8 Exclude .map files from the APK to squeeze them under the blob size limit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4277 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 13:24:01 +00:00
b0b0f74e83 Eek out a little more space on Android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4276 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 12:17:13 +00:00
d9eaa92c37 Messing with graph sizing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4275 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 12:00:50 +00:00
566d07117e Fix the android build.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4274 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 11:48:16 +00:00
2bffdb1168 Thought I had a fundamental UDP broadcast problem, but it was just bad setup in the test.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4273 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 03:18:12 +00:00
1359b48c9f Turn on -Wdouble-promotion. Why not.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4272 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 03:00:57 +00:00
a69fb5eeac I think this fixes posting.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4271 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-30 00:56:59 +00:00
38e313350e Trying to make the navigation bar resize right, but CSS doesn't like me.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4270 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 20:49:06 +00:00
5052dc04f2 Added spark line emojis and fixed some things about their rendering.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4269 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 19:46:33 +00:00
9ef3a3aca0 An experiment: Always show some stats as little sparklines at the top of the screen.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4268 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 19:27:00 +00:00
7b91a2ec37 Navigation bar => lit.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4267 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 18:23:08 +00:00
2926f855a1 Start using lit element in the main web interface. It's getting out of control, and if I can finish a refactor, it will reel it in.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4266 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-29 16:52:35 +00:00
639419db60 Oh freaking heck. This fixes the black bar at the bottom of the screen on Android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4265 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-20 00:24:12 +00:00
54747c127c Ugg. Android needs File.write.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4264 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-20 00:11:38 +00:00
791c3dd787 Remove unused file operations.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4263 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:53:52 +00:00
b00d75ab7c Fine. Fix windows.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4262 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:06:37 +00:00
956ea0df56 Track and expose hitches in some suspect callbacks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4261 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-19 23:05:59 +00:00
30014040e7 Update lit-all.min.js for ssb.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4260 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-16 21:36:17 +00:00
ab055c3394 I see what happened. codemirror 6.57.7 was really a misnumbered codemirror5 release. Let's go back to the latest codemirror5.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4259 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-16 13:07:02 +00:00
1e37eeea05 Experimenting with collapsing images.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4258 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-13 00:03:22 +00:00
84aec0278d Free earlier.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4257 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:28:58 +00:00
06642f58c5 One less blocking thing on the main thread: _tf_ssb_connection_send_history_stream.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4256 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:22:33 +00:00
e6d44b32f4 Seems we no longer need _tf_ssb_followingDeep.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4255 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 23:06:56 +00:00
1f3f6e2b92 Show audio: references inline, too, and now we don't have to show audio: and video: in the references section.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4254 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-12 00:32:14 +00:00
8f2d3e3bcd Show videos in messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4253 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-04-08 20:06:45 +00:00
2df2fc5792 This appears to avoid webview state loss when rotating.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4252 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:43:41 +00:00
20b0337e0a Hook up backtraces on android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4251 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:25:17 +00:00
e86b9dae48 Lint cleanup.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4250 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-29 22:02:12 +00:00
71de897419 Missing icon.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4249 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 14:33:52 +00:00
3edfaf9137 Add/enable codemirror's javascript-lint using jshint, and fix a few things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4248 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 00:46:40 +00:00
19c1784864 sqlite-amalgamation-3410200.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4247 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-25 00:13:39 +00:00
0d9fac7363 Support ?filename= to download a blob with a given filename.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4246 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-22 23:02:36 +00:00
2fb91fccc0 Extra /.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4245 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-22 01:21:22 +00:00
24e1ab12ab Maybe you're not signed in.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4244 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-22 01:20:57 +00:00
10ea885d8d Show the username in the apps list.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4243 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-22 01:19:53 +00:00
ec65faa12d Assign all stock apps an emoji, show them in the app list, and let the editor set them.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4242 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-21 23:08:04 +00:00
53692a1ea8 Trying to make the apps like work better on a phone.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4241 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-21 16:54:06 +00:00
ebef51b4ea Continue trying to make the android build smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4240 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-20 00:29:46 +00:00
a94d6f9271 Actually bind to whichever port.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4239 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-20 00:24:47 +00:00
3d2c88c201 Group contact messages, and try to fix some messages overflowing width.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4238 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-19 23:31:08 +00:00
bdeee7fc0e Trying mostly ineffectively to make android executables smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4237 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-19 20:25:50 +00:00
33a037e0ea Move executables out of the way where android expects native libraries to be.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4236 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-19 13:12:51 +00:00
2dc2d9ebf6 Add appstore, so I can get apps more easily to my phone.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4235 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-19 13:07:33 +00:00
9748f0ed8b Clean up out slightly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4234 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-18 12:31:58 +00:00
d6be2f7d54 Bind tildefriends HTTP to an arbitrary port, write it to a file, and have the Android activity notice that file write and load the correct URL.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4233 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-18 12:28:48 +00:00
63615747a7 Fix executable choosing for my phone, and fix broadcasting to each interface.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4232 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-18 01:26:34 +00:00
fbb657a85c Ugg, no actual change but I had to touch everything to get it working in the emulator again.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4231 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-17 23:48:54 +00:00
bdac0c7879 Whitespace.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4230 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-17 22:57:18 +00:00
54dde76a8a Optimize for size sometimes. APKs are part of all.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4229 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-16 00:44:22 +00:00
2bbe22bc7a Exclude some docs and things to get the release tar.xz back under 5MB.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4228 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-16 00:23:40 +00:00
ad8532f7ac Now actually include the code.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4227 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 23:57:35 +00:00
602941104e Support building both debug and release APKs. Release is too big.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4226 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 23:55:22 +00:00
d38b41687c Throw in the towel on swipe refresh and add a refresh button.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4225 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 23:08:57 +00:00
08125cd1e8 Fix the android code build again. Meh.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4224 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 22:14:21 +00:00
2ce2097a3f This works in the emulator.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4223 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 21:58:21 +00:00
a5da17e1b1 Use updated android tools? I don't know. Ugg.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4222 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-15 03:21:20 +00:00
2b0962f087 Add openssl for android x86_64, and build that executable into the APK as well. Not used yet.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4221 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-14 03:17:01 +00:00
37173cce4c Cut some things to make the APK smaller.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4220 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-14 02:39:25 +00:00
37edbd9824 Get forward and back gestures working, and hide the title bar. Hiding the action bar still eludes me.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4219 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-14 02:38:56 +00:00
a32bb02223 Various fixes I've accrued. Minor cleanups and more tracing in serialize. Turn off memory tracking. Fix Let's Encrypt.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4218 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-12 22:16:18 +00:00
2ab1b84432 sqlite-amalgamation-3410100.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4217 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-11 22:29:20 +00:00
52ae19220c Enable WebView prompts and localStorage and stuff.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4216 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-11 15:24:05 +00:00
10bfa65a4e Fixed apps not working most of the time. Ultimately, storing a pointer to the database using JS_NewInt64 was lossy and a bad idea. Also, remove use of JNI since we're only starting tildefriends as its own process now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4215 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-11 13:57:17 +00:00
2a3b1a1e33 So close. We can do it without the .so.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4214 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-11 03:47:01 +00:00
f74f4f6da9 First signs of WebView working.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4213 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-11 02:37:27 +00:00
12a8b7a058 Fix other platforms.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4212 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-10 02:06:23 +00:00
400f07660f Whoa. Apps are running on android. Switched to a static build of OpenSSL 1.1.1t for simplicity.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4211 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-10 02:02:24 +00:00
d532795b7f Import stock apps from the apk.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4210 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-09 01:39:48 +00:00
6064ed6a3a Don't use Secure cookies if we're not using TLS.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4209 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-09 01:39:15 +00:00
2c1a43df2e Implement enough of the File JS API to serve some web pages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4208 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-09 01:03:35 +00:00
bf72782c9f Now we're running enough code to respond (incorrectly) to http requests.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4207 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-09 00:32:42 +00:00
63dcab30c3 Now we can run scripts from a .zip.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4206 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-08 23:59:11 +00:00
50e48af7c4 Add all the files I think I need to the .apk, and add zlib, so I can attempt to access them using minizip.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4205 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-08 17:46:19 +00:00
9127a18ff0 With approximately this code, I was able to establish an SHS connection with my phone.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4204 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-08 02:49:41 +00:00
61ff466908 Replace all printfs with tf_printf, which redirects to android logging. Change into the files directory so that sqlite can do its thing. Getting closer.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4203 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-07 17:50:17 +00:00
1c10768aa4 Fix overbuild in android deps.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4202 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-07 03:02:16 +00:00
992b123853 Didn't end up using this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4201 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-05 02:54:29 +00:00
f736756b20 Make a JNI call.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4200 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-05 02:54:04 +00:00
28d73f5b37 Minimal build support for an android app. Written while the power was out.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4199 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-04 19:10:05 +00:00
262b0e5e52 Attempt to track CPU usage of libuv worker threads.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4198 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-03-01 01:36:26 +00:00
1e3807bcb9 Exposed functions to encrypt and decrypt private messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4197 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-26 19:51:54 +00:00
2ed3295f77 sqlite-amalgamation-3410000.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4196 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-26 03:12:14 +00:00
8c9d687d50 Variety of minor fixes I've been running with. SSB web interface changes. calloc overallocation fix. Use sqlAsync. Probably some other things.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4195 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-23 01:29:54 +00:00
b8b694864e Whoops, overallocated.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4194 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-20 02:42:11 +00:00
961109635b Latest libsodium-1.0.18-stable.tar.gz.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4193 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 23:23:53 +00:00
86bc46a11e Track memory allocations with a linked list. This is only about 3x slower than without tracking instead of 5x and growing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4192 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 22:28:36 +00:00
a6a6fe75ec Aha, one more leak in sqlAsync.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4191 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 13:51:06 +00:00
f55f863867 Some unused global variables.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4190 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 13:16:55 +00:00
4ce988d00b Memory leak in maskBytes.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4189 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 02:01:59 +00:00
1548a8a852 One less alloc for setTimeout.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4188 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-19 01:28:14 +00:00
a9551b057b Trace more things. Add a CORS header for /mem so I can make an app to examine it. Fix a memory leak. Fix tf_realloc(NULL, 0).
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4187 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-18 23:43:00 +00:00
88c7d91858 Brute force memory tracking.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4186 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-18 21:00:39 +00:00
53cb80ebf7 Replace the sqlite allocator, and use our own tracking for stats. Want to use this to collect callstacks for all allocations.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4185 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-18 19:14:06 +00:00
1f67343d75 Make traces work with multiple threads, I think.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4184 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-18 00:51:22 +00:00
4bea8bb6ba sqlite thread safety and extended result codes, mainly.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4183 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-17 22:43:19 +00:00
8e1461b3f1 Catch more sqlite errors.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4182 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-17 02:04:48 +00:00
90b513d070 Fix syntax errors not propagating.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4181 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-17 01:42:56 +00:00
8a2d3d4669 Pass around SQL errors slightly better.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4180 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-16 00:06:45 +00:00
1741403206 More memory leaks.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4179 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-15 02:59:46 +00:00
980db880cc Memory leak.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4178 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-15 02:56:01 +00:00
507a62539d Fix exporting.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4177 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-15 02:43:08 +00:00
6b5d73ed5c Vague attempt at some more cleanup, and stick pthread_self() in the traces.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4176 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-15 02:34:46 +00:00
1f77df7a90 Remove dependency on base64c. Use libsodium's. Also consolidate the calls, as the usage is quite special.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4175 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-14 03:15:24 +00:00
fa87462405 Finish writing this code. Yep.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4174 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-14 02:13:08 +00:00
a5f9f927e6 Fix some memory leaks I just introduced.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4173 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-08 01:50:47 +00:00
b35d74ce36 Allow running read-only sqlite queries from libuv worker threads. Needs so much more testing.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4172 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-08 01:29:44 +00:00
ac60be14a5 Sure, we can identify SVG files.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4171 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-07 23:39:04 +00:00
beda047eb0 Disable Nagle's algorithm before we start the TLS handshake. Just speculation that it will help with some responsiveness.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4170 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-06 02:29:00 +00:00
f6742bebf3 Tracing will continue until performance improves.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4169 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-03 15:06:18 +00:00
7f334ad783 Fine, only malloc_trim if it looks like we have it.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4168 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-03 14:20:26 +00:00
ffda896308 Finish import.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4167 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-03 14:09:53 +00:00
b2fbe9dfac Stale doc file. Fix hashtag links. Trace some GC stuff and try malloc_trim, whynot.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4166 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-03 14:01:05 +00:00
6d6c41bffa Oops. Cleanup.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4165 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-02 02:48:07 +00:00
e04d137af5 Refactored import and export. No user on disk.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4164 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-02 02:09:05 +00:00
ec52e62908 Move apps/cory/ => apps/. Going to change import and export to support this.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4163 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-02 00:18:22 +00:00
6104af0d70 Smaller docker image. Why not.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4162 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-01 23:47:07 +00:00
0ca05e297d No more global settings file.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4161 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-01 23:40:21 +00:00
e0dcec074c Add process name to trace.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4160 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-01 23:20:16 +00:00
a8cecb5c64 Fix trace producing invalid JSON.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4159 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-02-01 03:15:22 +00:00
582ee0e4d7 var => let
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4158 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-01-31 02:48:56 +00:00
0ba54c2b7b Update lit element. Better drafts. Compose content warnings.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4157 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-01-30 01:45:23 +00:00
3c288f7f68 Remove duplicate apps entries on import.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4156 ed5197a5-7fde-0310-b194-c3ffbd925b24
2023-01-29 01:58:57 +00:00
16454 changed files with 394346 additions and 38608 deletions

View File

@ -2,8 +2,10 @@ FROM bitnami/minideb:bullseye AS build
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev
gcc \
libc6-dev \
libssl-dev \
make
COPY . /app
RUN make -C /app -j $(nproc) release

253
Makefile
View File

@ -5,7 +5,7 @@ MAKEFLAGS += --no-builtin-rules
PROJECT = tildefriends
BUILD_DIR ?= out
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
UNAME_M := $(shell uname -m)
CFLAGS += \
@ -16,17 +16,54 @@ CFLAGS += \
-MMD \
-ffunction-sections \
-fdata-sections \
-fno-omit-frame-pointer \
-fno-exceptions \
-g
LDFLAGS += -Wl,--gc-sections
NDK_PATH := /usr/lib/android-sdk/ndk-bundle
NDK_API_VERSION := 30
NDK_TARGET_TRIPLE := aarch64-linux-android
ANDROID_SDK ?= ~/Android/Sdk
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
ANDROID_NDK_API_VERSION := 31
ANDROID_MIN_SDK_VERSION := 26
debug windebug androiddebug: CFLAGS += -Og
debug release androidrelease: LDFLAGS += -rdynamic
release winrelease: CFLAGS += -DNDEBUG -O3
ANDROID_ARM64_TARGETS := \
out/androiddebug/tildefriends \
out/androidrelease/tildefriends
ANDROID_X86_64_TARGETS := \
out/androiddebug-x86_64/tildefriends \
out/androidrelease-x86_64/tildefriends
ANDROID_TARGETS := \
$(ANDROID_X86_64_TARGETS) \
$(ANDROID_ARM64_TARGETS)
DEBUG_TARGETS := \
out/debug/tildefriends \
out/windebug/tildefriends \
out/androiddebug/tildefriends \
out/androiddebug-x86_64/tildefriends
RELEASE_TARGETS := \
out/release/tildefriends \
out/winrelease/tildefriends \
out/androidrelease/tildefriends \
out/androidrelease-x86_64/tildefriends
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
$(ANDROID_TARGETS): CFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-fPIC \
-fdebug-compilation-dir . \
-fomit-frame-pointer \
-fno-asynchronous-unwind-tables
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
windebug winrelease: AS = $(CC)
windebug winrelease: CFLAGS += \
@ -38,15 +75,17 @@ windebug winrelease: LDFLAGS += \
-static \
-lm \
-Ldeps/openssl/mingw64/lib
androiddebug androidrelease: CC = $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
androiddebug androidrelease: AS = $(CC)
androiddebug androidrelease: CFLAGS += \
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
-Ideps/openssl/android/arm64-v8a/usr/local/include \
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
$(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-Wno-unknown-warning-option
androiddebug androidrelease: LDFLAGS += \
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
-Ldeps/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
ifeq ($(UNAME_M),x86_64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
@ -57,8 +96,8 @@ get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
APP_SOURCES := $(wildcard src/*.c)
APP_OBJS := $(call get_objs,APP_SOURCES)
@ -69,18 +108,16 @@ $(APP_OBJS): CFLAGS += \
-Ideps/libsodium \
-Ideps/libsodium/src/libsodium/include \
-Ideps/libuv/include \
-Ideps/zlib \
-Ideps/zlib/contrib/minizip \
-Ideps/picohttpparser \
-Ideps/quickjs \
-Ideps/sqlite \
-Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \
-Werror
BASE64C_SOURCES := deps/base64c/src/base64c.c
BASE64C_OBJS := $(call get_objs,BASE64C_SOURCES)
$(BASE64C_OBJS): CFLAGS += \
-Wno-sign-compare
BLOWFISH_SOURCES := \
deps/crypt_blowfish/crypt_blowfish.c \
deps/crypt_blowfish/crypt_gensalt.c \
@ -105,13 +142,10 @@ UV_SOURCES_unix := \
deps/libuv/src/unix/async.c \
deps/libuv/src/unix/core.c \
deps/libuv/src/unix/dl.c \
deps/libuv/src/unix/epoll.c \
deps/libuv/src/unix/fs.c \
deps/libuv/src/unix/getaddrinfo.c \
deps/libuv/src/unix/getnameinfo.c \
deps/libuv/src/unix/linux-core.c \
deps/libuv/src/unix/linux-inotify.c \
deps/libuv/src/unix/linux-syscalls.c \
deps/libuv/src/unix/linux.c \
deps/libuv/src/unix/loop-watcher.c \
deps/libuv/src/unix/loop.c \
deps/libuv/src/unix/pipe.c \
@ -129,7 +163,6 @@ UV_SOURCES_unix := \
deps/libuv/src/unix/tty.c \
deps/libuv/src/unix/udp.c
UV_SOURCES_android := \
deps/libuv/src/unix/pthread-fixes.c \
deps/libuv/src/unix/random-getentropy.c
UV_SOURCES_win := \
deps/libuv/src/win/async.c \
@ -161,12 +194,13 @@ UV_OBJS := $(call get_objs,UV_SOURCES)
$(UV_OBJS): CFLAGS += \
-Ideps/libuv/include \
-Ideps/libuv/src \
-Wno-unused-but-set-variable \
-Wno-incompatible-pointer-types \
-Wno-sign-compare \
-Wno-unused-variable \
-Wno-dangling-pointer \
-Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \
-Wno-sign-compare \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable \
-D_GNU_SOURCE
SODIUM_SOURCES := \
@ -207,6 +241,7 @@ SODIUM_SOURCES := \
deps/libsodium/src/libsodium/randombytes/randombytes.c \
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
deps/libsodium/src/libsodium/sodium/core.c \
deps/libsodium/src/libsodium/sodium/codecs.c \
deps/libsodium/src/libsodium/sodium/runtime.c \
deps/libsodium/src/libsodium/sodium/utils.c
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
@ -223,23 +258,37 @@ SQLITE_SOURCES := deps/sqlite/sqlite3.c
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
$(SQLITE_OBJS): CFLAGS += \
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
-DSQLITE_DEFAULT_MEMSTATUS=0 \
-DSQLITE_DQS=0 \
-DSQLITE_ENABLE_MEMSYS5 \
-DSQLITE_ENABLE_FTS5 \
-DSQLITE_ENABLE_JSON1 \
-DSQLITE_MAX_LENGTH=5242880 \
-DSQLITE_MAX_SQL_LENGTH=100000 \
-DSQLITE_MAX_COLUMN=100 \
-DSQLITE_MAX_EXPR_DEPTH=40 \
-DSQLITE_MAX_COMPOUND_SELECT=300 \
-DSQLITE_MAX_VDBE_OP=25000 \
-DSQLITE_MAX_FUNCTION_ARG=8 \
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
-DSQLITE_MAX_ATTACHED=0 \
-DSQLITE_MAX_COLUMN=100 \
-DSQLITE_MAX_COMPOUND_SELECT=300 \
-DSQLITE_MAX_EXPR_DEPTH=40 \
-DSQLITE_MAX_FUNCTION_ARG=8 \
-DSQLITE_MAX_LENGTH=5242880 \
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
-DSQLITE_MAX_SQL_LENGTH=100000 \
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
-DSQLITE_MAX_VDBE_OP=25000 \
-DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_DESERIALIZE \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_OMIT_TCL_VARIABLE \
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
-DSQLITE_SECURE_DELETE \
-DSQLITE_THREADSAFE=0 \
-DSQLITE_UNTESTABLE \
-DSQLITE_USE_ALLOCA \
-DHAVE_ISNAN \
-Wno-implicit-fallthrough \
-Wno-unused-but-set-variable \
-Wno-unused-function
-Wno-unused-function \
-Wno-unused-variable
XOPT_SOURCES := deps/xopt/xopt.c
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
@ -249,25 +298,27 @@ $(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
-DHAVE_VASNPRINTF \
-DHAVE_VASPRINTF \
-Dvsnprintf=rpl_vsnprintf
$(XOPT_OBJS): CFLAGS += \
-Wno-implicit-const-int-float-conversion
QUICKJS_SOURCES := \
deps/quickjs/cutils.c \
deps/quickjs/libbf.c \
deps/quickjs/libregexp.c \
deps/quickjs/libunicode.c \
deps/quickjs/quickjs-libc.c \
deps/quickjs/quickjs.c
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
$(QUICKJS_OBJS): CFLAGS += \
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
-DCONFIG_BIGNUM \
-DDUMP_LEAKS \
-D_GNU_SOURCE \
-Wno-sign-compare \
-Wno-enum-conversion \
-Wno-implicit-const-int-float-conversion \
-Wno-implicit-fallthrough \
-Wno-unused-variable \
-Wno-sign-compare \
-Wno-unused-but-set-variable \
-Wno-enum-conversion
-Wno-unused-variable
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
LIBBACKTRACE_SOURCES := \
deps/libbacktrace/atomic.c \
@ -299,7 +350,20 @@ $(LIBBACKTRACE_OBJS): CFLAGS += \
PICOHTTPPARSER_SOURCES := \
deps/picohttpparser/picohttpparser.c
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
# $(PICOHTTPPARSER_OBJS): CFLAGS +=
MINIUNZIP_SOURCES := \
deps/zlib/contrib/minizip/unzip.c \
deps/zlib/contrib/minizip/ioapi.c \
deps/zlib/adler32.c \
deps/zlib/crc32.c \
deps/zlib/inffast.c \
deps/zlib/inflate.c \
deps/zlib/inftrees.c \
deps/zlib/zutil.c
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
$(MINIUNZIP_OBJS): CFLAGS += \
-Ideps/zlib \
-Wno-maybe-uninitialized
LDFLAGS += \
-pthread \
@ -309,30 +373,34 @@ debug release: LDFLAGS += \
-lssl \
-lcrypto
windebug winrelease: LDFLAGS += \
-lwsock32 \
-lws2_32 \
-lkernel32 \
-liphlpapi \
-luserenv \
-lssl \
-lcrypto \
-lcrypt32 \
-ldbghelp \
-liphlpapi \
-lkernel32 \
-lole32 \
-luserenv \
-luuid \
-lws2_32 \
-lcrypt32
androiddebug androidrelease: LDFLAGS += \
-lwsock32
$(ANDROID_TARGETS): LDFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-ldl \
-llog \
-lssl \
-lcrypto
unix: debug release
win: windebug winrelease
all: $(BUILD_TYPES)
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
.PHONY: all win unix
ALL_APP_OBJS := \
$(APP_OBJS) \
$(BASE64C_OBJS) \
$(BLOWFISH_OBJS) \
$(LIBBACKTRACE_OBJS) \
$(MINIUNZIP_OBJS) \
$(PICOHTTPPARSER_OBJS) \
$(QUICKJS_OBJS) \
$(SODIUM_OBJS) \
@ -349,7 +417,7 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
@echo [link] $$@
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
$(BUILD_DIR)/$(1)/%.o: %.c
@mkdir -p $$(dir $$@)
@ -364,6 +432,77 @@ endef
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
# Android support.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@mkdir -p $(dir $@)
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
$(CLASS_FILES) &: $(JAVA_FILES)
@echo [javac] $(CLASS_FILES)
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
out/apk/classes.dex: $(CLASS_FILES)
@mkdir -p $(dir $@)
@echo [d8] $@
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
PACKAGE_DIRS := \
apps/ \
core/ \
deps/codemirror/ \
deps/lit/ \
deps/split/ \
deps/smoothie/
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
out/%.unsigned.apk:
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
@echo [aapt] $@
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
@cp out/apk/res.apk $@
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
out/%.apk: out/apk/%.unsigned.apk
@echo [apksigner] $(notdir $@)
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
apk: out/TildeFriends-debug.apk
.PHONY: apk
apkgo: out/TildeFriends-debug.apk
@adb install $<
@adb shell am start com.unprompted.tildefriends/.MainActivity
.PHONY: apkgo
apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean

View File

@ -28,7 +28,7 @@ privileges. Further administration can be done at
<http://localhost:12345/~core/admin/`>.
## Documentation
There are the very beginnings of developer documentation in `apps/cory/docs/`
There are the very beginnings of developer documentation in `apps/docs/`
that can be read in-place or at <http://localhost:12345/~core/docs/>.
## License

4
apps/admin.json Normal file
View File

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

4
apps/api.json Normal file
View File

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

View File

@ -1,4 +1,3 @@
var global = Function('return this')();
function treeify(o) {
if (typeof(o) == 'object') {
return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
@ -8,4 +7,4 @@ function treeify(o) {
return o;
}
}
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(global), null, 2)}</pre>`);
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(globalThis), null, 2)}</pre>`);

4
apps/apps.json Normal file
View File

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

77
apps/apps/app.js Normal file
View File

@ -0,0 +1,77 @@
async function fetch_info(apps) {
let result = {};
for (let [key, value] of Object.entries(apps)) {
let blob = await ssb.blobGet(value);
blob = blob ? utf8Decode(blob) : '{}';
result[key] = JSON.parse(blob);
}
return result;
}
async function main() {
var apps = await fetch_info(await core.apps());
var core_apps = await fetch_info(await core.apps('core'));
var doc = `<!DOCTYPE html>
<html>
<head>
<style>
.container {
display: grid;
grid-template-columns: repeat(auto-fill, 64px);
justify-content: space-around;
}
.app {
height: 96px;
width: 64px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
white-space: nowrap;
}
.app > a {
text-decoration: none;
max-width: 64px;
text-overflow: ellipsis ellipsis;
overflow: hidden;
}
</style>
</head>
<body style="background: #888">
<h1 id="apps_title">Apps</h1>
<div id="apps" class="container"></div>
<h1>Core Apps</h1>
<div id="core_apps" class="container"></div>
</body>
<script>
function populate_apps(id, name, apps) {
var list = document.getElementById(id);
for (let app of Object.keys(apps).sort()) {
let div = list.appendChild(document.createElement('div'));
div.classList.add('app');
let icon_a = document.createElement('a');
let icon = document.createElement('div');
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
icon.style.fontSize = 'xxx-large';
icon_a.appendChild(icon);
icon_a.href = '/~' + name + '/' + app + '/';
icon_a.target = '_top';
div.appendChild(icon_a);
let a = document.createElement('a');
a.appendChild(document.createTextNode(app));
a.href = '/~' + name + '/' + app + '/';
a.target = '_top';
div.appendChild(a);
}
}
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
</script>
</html>`;
app.setDocument(doc);
}
main();

4
apps/appstore.json Normal file
View File

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

55
apps/appstore/app.js Normal file
View File

@ -0,0 +1,55 @@
async function get_apps() {
let results = {};
await ssb.sqlAsync(`
SELECT messages.*
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp
`,
[],
function(row) {
let content = JSON.parse(row.content);
for (let mention of content.mentions) {
if (mention?.type === 'application/tildefriends') {
results[JSON.stringify([row.author, mention.name])] = {
message: row,
blob: mention.link,
name: mention.name,
};
}
}
});
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
}
function render_app(app) {
return `
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
<a href="/~cory/ssb/#${app.message.author}">@</a>
<a href="/~cory/ssb/#${app.message.id}">%</a>
<a href="/${app.blob}/">${app.name}</a>
</div>
`;
}
async function main() {
let apps = await get_apps();
app.setDocument(`
<html>
<head>
<base target="_top">
<style>
a:link { color: #bbf; }
a:visited { color: #ddd; }
a:hover { color: #ddf; }
</style>
</head>
<body style="color: #fff">
<h1>${apps.length} apps</h1>
${apps.map(render_app).join('\n')}
</body>
</html>
`);
}
main();

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&uhGJsy5+qBgOgEgMqCTDasK+C+GWGptHKfPiAsD5eGA=.sha256","index.html":"&D3JwdPXy/QsLXkmwNDrBFXdzxfqO1/JGxfqEArnS5v4=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&TZ2ymD6cFVUjQleGcDslt8apjp7k3xLlfv2F8rQVM4I=.sha256"}}

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&qEJDfZ43KazIxiZl8OCKb2uaDOsPkxnIohEzQ1LLFpg=.sha256"}}

View File

@ -1,31 +0,0 @@
async function main() {
var apps = await core.apps();
var core_apps = await core.apps('core');
var doc = `<!DOCTYPE html>
<html>
<body style="background: #888">
<h1>Apps</h1>
<ul id="apps"></ul>
<h1>Core Apps</h1>
<ul id="core_apps"></ul>
</body>
<script>
function populate_apps(id, name, apps) {
var list = document.getElementById(id);
for (let app of Object.keys(apps).sort()) {
var li = list.appendChild(document.createElement('li'));
var a = document.createElement('a');
a.innerText = app;
a.href = '/~' + name + '/' + app + '/';
a.target = '_top';
li.appendChild(a);
}
}
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
</script>
</html>`
app.setDocument(doc);
}
main();

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&WEvJYebSMi5d2eXgUwJJmvR/Q4slFg3zHYB8Q2mXJII=.sha256","index.md":"&79+ntX4sRvg+MboV5nMFz01BSicxsWIQRx719VHS8uk=.sha256","todo.md":"&hQABwP24zFFhdHagRMF3Am7rV2yH19e+0xJ4wnZ4kfM=.sha256","structure.md":"&jph8x/fMXKOd4I0ZiUVb0ZLTfPQ7gBWoxJPrvtX6vtw=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","ssb.md":"&JH1JfoTaCcUifCpnAwhImKBACI0PHoLhoOw1WAnWpLw=.sha256","vision.md":"&v2wu2MGlhNvaALQQ9rGna7ZeEQWSghFgQcDfD5xEyE0=.sha256"}}

View File

@ -1,17 +0,0 @@
# ID Refactor
[Back to index](#index)
## Goals
- no way to get private key in javascript
- ssb.c syncs/broadcasts/... efficiently for everybody
## Schema
- separate table to discourage leakage
- `CREATE TABLE identities (user TEXT, public TEXT, secret TEXT);`
## API
- `ssb.createIdentity()` -> `id`
- `ssb.getIdentities()` => `[id, ...]`
- `ssb.deleteIdentity(id)`
- `ssb.post(id, ...)`
- `ssb.appendMessage(id, ...)`

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&3d9ABFgRwQvWsYbFv/rzimtnLDnVrWlGtdw7serFIGw=.sha256"}}

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&1HWTkyCc1doft6dyKF5FDxtRAErNeY25CBrfZbKPpyo=.sha256","lit-all.min.js":"&XKgdRySJuiZeZvchNFGjVWn0XOVhQFmG7/HTWYQ8s68=.sha256","index.html":"&TxhFekB9ov7tf/fmkAg7x5797i27oLidhgxEfDKC0T0=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&lA9iFp1YbqSndxXZuwtgmrj7NDMkN71nJITbtjWL3VA=.sha256","tf-id-picker.js":"&maN8DUFrmRxW5nsVyOAMk5k1ekcz/pfzvSS99ac3jo8=.sha256","tf-app.js":"&F0fyawIO410YFidrzFjlHeY++sZy6ledf6CAXB+45U4=.sha256","tf-message.js":"&HToh+7UCoanBzlr/TEsy/JG4OS2IBU1tMuzjuNmUkAo=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&lYNeL7cVlDgcqrfkoRIe69DHZeqSZMiHhZIieblHbU0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&7HZLHf5NB5hE6FW0hiXNvM17ekGBn5BBle1bvnjVjyo=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&tOkUocccQWBzkNzSEf9VMltkTSHcUALYSPYVWmJMoBc=.sha256","tf-styles.js":"&LFeL/vWgrv4N8q/mBrQAnhbaOI+dXNJYvH9bn1bXSqQ=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&fY+thANurOKU2/RhDt411ZtkxW0nV24+hLEf00Z1sTY=.sha256","tf-tab-connections.js":"&ywqBz3w63R6naH09kZ+01A0SfmtuSfk8QPBXWsli0yg=.sha256","tf-news.js":"&Zn+vxLUqVJbo/q6RcW8ezvbdilzllvXhZRyXk8kYwL0=.sha256","tribute.css":"&9FogMzZHKXCfGb7mlh7z+/wiNZzBsOB/tKoh6MfYJno=.sha256","tribute.esm.js":"&P1wKqCfYULpR/ahSB98JP8xaxfikuZwwtT6I/SAo7/Y=.sha256","commonmark-hashtag.js":"&fudY0YdvcMjVCSZ0oiCqUt0+bVT0a06j5TcjWaCDO8E=.sha256"}}

File diff suppressed because it is too large Load Diff

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,57 +0,0 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfConnectionsElement extends LitElement {
static get properties() {
return {
broadcasts: {type: Array},
identities: {type: Array},
connections: {type: Array},
users: {type: Object},
}
}
constructor() {
super();
let self = this;
this.broadcasts = [];
this.identities = [];
this.connections = [];
this.users = {};
tfrpc.rpc.getAllIdentities().then(function(identities) {
self.identities = identities || [];
});
}
_emit_change() {
let changed_event = new Event('change', {
srcElement: this,
});
this.dispatchEvent(changed_event);
}
changed(event) {
this.selected = event.srcElement.value;
tfrpc.rpc.localStorageSet('whoami', this.selected);
this._emit_change();
}
render() {
return html`
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
</ul>
`;
}
}
customElements.define('tf-connections', TfConnectionsElement);

View File

@ -1,48 +0,0 @@
import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js';
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
parsed = hashtagify.transform(parsed);
var walker = parsed.walker();
var event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}
export function human_readable_size(bytes) {
let v = bytes;
let u = 'B';
for (let unit of ['kB', 'MB', 'GB']) {
if (v > 1024) {
v /= 1024;
u = unit;
} else {
break;
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&QUR1tKa15B5Or8AfPX/8Zs87teSeX0Mh/HF7PEPBom0=.sha256","index.html":"&QXhwvxhHc9fa8iL6088hGDu9FgWdY7wkXgvU2BMNv0A=.sha256","lit-core.min.js":"&tP9KhbgwF1chFqPtkNZ12Yx9AfkpnSjFiPcX5Pw5J9g=.sha256","script.js":"&KgOaUVjBM4MzSy7PpUVQHETuvgXAx2JGPJABksBg+QY=.sha256"}}

4
apps/db.json Normal file
View File

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

View File

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

4
apps/docs.json Normal file
View File

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

View File

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

4
apps/follow.json Normal file
View File

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

View File

@ -1,5 +1,3 @@
"use strict";
var g_following_cache = {};
var g_following_deep_cache = {};
var g_about_cache = {};
@ -15,7 +13,7 @@ async function following(db, id) {
f = {users: [], sequence: 0, version: k_version};
}
f.users = new Set(f.users);
await ssb.sqlStream(
await ssb.sqlAsync(
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
@ -73,7 +71,7 @@ async function getAbout(db, id) {
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlStream(
await ssb.sqlAsync(
"SELECT "+
" sequence, "+
" content "+
@ -109,7 +107,7 @@ async function getAbout(db, id) {
async function getSize(db, id) {
let size = 0;
await ssb.sqlStream(
await ssb.sqlAsync(
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
[id],
function (row) {

4
apps/sneaker.json Normal file
View File

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

4
apps/ssb.json Normal file
View File

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

View File

@ -47,7 +47,7 @@ tfrpc.register(async function closeConnection(id) {
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlStream(sql, args, function callback(row) {
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
@ -79,9 +79,15 @@ tfrpc.register(async function store_blob(blob) {
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
tfrpc.register(async function store_message(message) {
return await ssb.storeMessage(message);
});
tfrpc.register(function apps() {
return core.apps();
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
ssb.addEventListener('broadcasts', async function() {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});

View File

@ -39,7 +39,7 @@ function splitMatches(text, regexp) {
return result;
}
const regex = new RegExp("\\W#[\\w-]+");
const regex = new RegExp("(?<!\w)#[\\w-]+");
function split(textNodes) {
const text = textNodes.map(n => n.literal).join("");

View File

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

1
apps/ssb/emojis.json Normal file

File diff suppressed because one or more lines are too long

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

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

View File

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,5 +9,6 @@ import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.js';
import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';

View File

@ -32,8 +32,8 @@ class TfElement extends LitElement {
this.following = [];
this.users = {};
this.loaded = false;
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) {
self.set_hash(hash);
@ -79,7 +79,7 @@ class TfElement extends LitElement {
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, "$.type") = "contact"
json_extract(content, '$.type') = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]);
@ -133,7 +133,11 @@ class TfElement extends LitElement {
`, []))[0].max_row_id;
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
cache.last_row_id = max_row_id;
await tfrpc.rpc.databaseSet('following', JSON.stringify(cache));
let store = JSON.stringify(cache);
/* 2023-02-20: Exceeding message size. */
//if (store.length < 512 * 1024) {
await tfrpc.rpc.databaseSet('following', store);
//}
return [result, cache.following];
}
@ -276,16 +280,6 @@ class TfElement extends LitElement {
}
}
add_fake_news() {
this.unread = [{
author: this.whoami,
placeholder: true,
id: '%fake_id',
text: 'text',
content: 'hello',
}, ...this.unread];
}
async set_tab(tab) {
this.tab = tab;
if (tab === 'news') {
@ -322,7 +316,6 @@ class TfElement extends LitElement {
return html`
${this.render_id_picker()}
${tabs}
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
${contents}
`;
}

View File

@ -11,10 +11,9 @@ class TfComposeElement extends LitElement {
users: {type: Object},
root: {type: String},
branch: {type: String},
mentions: {type: Object},
apps: {type: Object},
drafts: {type: Object},
}
};
}
static styles = styles;
@ -24,7 +23,6 @@ class TfComposeElement extends LitElement {
this.users = {};
this.root = undefined;
this.branch = undefined;
this.mentions = {};
this.apps = undefined;
this.drafts = {};
}
@ -34,6 +32,8 @@ class TfComposeElement extends LitElement {
return '';
}
/* Update mentions. */
let draft = this.get_draft();
let updated = false;
for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
let name = match[1];
let link = match[2];
@ -50,14 +50,19 @@ class TfComposeElement extends LitElement {
break;
}
}
if (!this.mentions[link]) {
this.mentions[link] = {
link: link,
}
if (!draft.mentions) {
draft.mentions = {};
}
this.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
this.mentions = Object.assign({}, this.mentions);
console.log(this.mentions);
if (!draft.mentions[link]) {
draft.mentions[link] = {
link: link,
};
}
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
updated = true;
}
if (updated) {
this.requestUpdate();
}
return tfutils.markdown(text);
}
@ -66,6 +71,11 @@ class TfComposeElement extends LitElement {
let edit = this.renderRoot.getElementById('edit');
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value);
let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
}
notify(draft) {
@ -79,9 +89,11 @@ class TfComposeElement extends LitElement {
}));
}
change(event) {
let edit = this.renderRoot.getElementById('edit');
this.notify(edit.value);
change() {
let draft = this.get_draft();
draft.text = this.renderRoot.getElementById('edit')?.value;
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
this.notify(draft);
}
convert_to_format(buffer, type, mime_type) {
@ -99,7 +111,7 @@ class TfComposeElement extends LitElement {
let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
resolve(result);
}
};
img.onerror = function(event) {
reject(new Error('Failed to load image.'));
};
@ -111,6 +123,7 @@ class TfComposeElement extends LitElement {
async add_file(file) {
try {
let draft = this.get_draft();
let self = this;
let buffer = await file.arrayBuffer();
let type = file.type;
@ -131,16 +144,19 @@ class TfComposeElement extends LitElement {
}
let id = await tfrpc.rpc.store_blob(buffer);
let name = type.split('/')[0] + ':' + file.name;
self.mentions[id] = {
if (!draft.mentions) {
draft.mentions = {};
}
draft.mentions[id] = {
link: id,
name: name,
type: type,
size: buffer.length ?? buffer.byteLength,
};
self.mentions = Object.assign({}, self.mentions);
let edit = self.renderRoot.getElementById('edit');
edit.value += `\n![${name}](${id})`;
self.change();
self.input();
} catch(e) {
alert(e?.message);
}
@ -162,6 +178,7 @@ class TfComposeElement extends LitElement {
submit() {
let self = this;
let draft = this.get_draft();
let edit = this.renderRoot.getElementById('edit');
let message = {
type: 'post',
@ -171,15 +188,18 @@ class TfComposeElement extends LitElement {
message.root = this.root;
message.branch = this.branch;
}
if (Object.values(this.mentions).length) {
message.mentions = Object.values(this.mentions);
if (Object.values(draft.mentions || {}).length) {
message.mentions = Object.values(draft.mentions);
}
if (draft.content_warning !== undefined) {
message.contentWarning = draft.content_warning;
}
console.log('Would post:', message);
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
edit.value = '';
self.mentions = {};
self.change();
self.notify(undefined);
self.requestUpdate();
}).catch(function(error) {
alert(error.message);
});
@ -216,17 +236,38 @@ class TfComposeElement extends LitElement {
tribute.attach(this.renderRoot.getElementById('edit'));
}
updated() {
super.updated();
let edit = this.renderRoot.getElementById('edit');
if (this.last_updated_text !== edit.value) {
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value);
this.last_updated_text = edit.value;
}
}
remove_mention(id) {
delete this.mentions[id];
this.mentions = Object.assign({}, this.mentions);
let draft = this.get_draft();
delete draft.mentions[id];
this.notify(draft);
this.requestUpdate();
}
render_mention(mention) {
let self = this;
return html`
<div>
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter(x => x[0] != 'name')
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
</div>
</div>
</div>`;
}
@ -251,8 +292,11 @@ class TfComposeElement extends LitElement {
};
}
}
this.mentions = Object.assign(this.mentions || {}, mentions);
this.apps = null;
let draft = self.get_draft();
draft.mentions = Object.assign(draft.mentions || {}, mentions);
self.requestUpdate();
self.notify(draft);
self.apps = null;
}
if (this.apps) {
@ -269,24 +313,64 @@ class TfComposeElement extends LitElement {
}
render_attach_app_button() {
let self = this;
async function attach_app() {
this.apps = await tfrpc.rpc.apps();
self.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
} else {
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
}
}
set_content_warning(value) {
let draft = this.get_draft();
draft.content_warning = value;
this.notify(draft);
this.requestUpdate();
}
render_content_warning() {
let self = this;
let draft = this.get_draft();
if (draft.content_warning !== undefined) {
return html`
<div>
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
<label for="cw">CW</label>
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
</div>
`;
} else {
return html`
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label>
`;
}
}
get_draft() {
return this.drafts[this.branch || ''] || {};
}
render() {
let self = this;
let draft = self.get_draft();
let content_warning =
draft.content_warning !== undefined ?
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
undefined;
let result = html`
<div style="display: flex; flex-direction: row; width: 100%">
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${this.drafts[this.branch || '']}</textarea>
<div id="preview" style="flex: 1 0 50%"></div>
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
<div style="flex: 1 0 50%">
${content_warning}
<div id="preview"></div>
</div>
</div>
${Object.values(this.mentions).map(x => self.render_mention(x))}
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_content_warning()}
${this.render_attach_app()}
<input type="button" value="Submit" @click=${this.submit}></input>
<input type="button" value="Attach" @click=${this.attach}></input>

View File

@ -9,7 +9,7 @@ class TfIdentityPickerElement extends LitElement {
return {
ids: {type: Array},
selected: {type: String},
}
};
}
constructor() {

View File

@ -14,7 +14,8 @@ class TfMessageElement extends LitElement {
raw: {type: Boolean},
blog_data: {type: String},
expanded: {type: Object},
}
decrypted: {type: Object},
};
}
static styles = styles;
@ -28,6 +29,7 @@ class TfMessageElement extends LitElement {
this.drafts = {};
this.raw = false;
this.expanded = {};
this.decrypted = undefined;
}
show_reply() {
@ -69,12 +71,12 @@ class TfMessageElement extends LitElement {
hash: this.message?.hash,
content: this.message?.content,
signature: this.message?.signature,
}
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
};
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
}
vote(emoji) {
let reaction = emoji.emoji;
let reaction = emoji;
let message = this.message.id;
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
tfrpc.rpc.appendMessage(
@ -127,6 +129,13 @@ class TfMessageElement extends LitElement {
body_click(event) {
if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src);
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
let next = event.srcElement.nextSibling;
if (next.style.display == 'block') {
next.style.display = 'none';
} else {
next.style.display = 'block';
}
}
}
@ -148,7 +157,7 @@ class TfMessageElement extends LitElement {
} else if (mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')) {
return html`
<video controls style="max-height: 240px">
<video controls style="max-height: 240px; max-width: 128px">
<source src=${'/' + mention.link + '/view'}></source>
</video>
`;
@ -168,10 +177,7 @@ class TfMessageElement extends LitElement {
render_mentions() {
let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(x =>
x.name?.startsWith('audio:') ||
x.name?.startsWith('video:') ||
this.message?.content?.text?.indexOf(x.link) === -1);
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
if (mentions.length) {
let self = this;
return html`
@ -214,24 +220,43 @@ class TfMessageElement extends LitElement {
}
}
async try_decrypt(content) {
let result = await tfrpc.rpc.try_decrypt(this.whoami, content);
if (result) {
this.decrypted = JSON.parse(result);
} else {
this.decrypted = false;
}
}
render() {
let content = this.message?.content;
if (this.decrypted?.type == 'post') {
content = this.decrypted;
}
let self = this;
let raw_button = this.raw ?
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
function small_frame(inner) {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
${self.raw ? self.render_raw() : inner}
${self.render_votes()}
</div>
`
`;
}
if (this.message.placeholder) {
if (this.message?.type === 'contact_group') {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
${this.message.messages.map(x =>
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
)}
</div>`;
} else if (this.message.placeholder) {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
@ -258,7 +283,7 @@ class TfMessageElement extends LitElement {
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
</div>
`
`;
}
let update = content.about == this.message.author ?
html`<div style="font-weight: bold">Updated profile.</div>` :
@ -270,8 +295,9 @@ class TfMessageElement extends LitElement {
${description}
`);
} else if (content.type == 'contact') {
return small_frame(html`
return html`
<div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
is
${
content.blocking === true ? 'blocking' :
@ -282,7 +308,7 @@ class TfMessageElement extends LitElement {
}
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
</div>
`);
`;
} else if (content.type == 'post') {
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
<tf-compose
@ -300,7 +326,7 @@ class TfMessageElement extends LitElement {
this.render_raw() :
unsafeHTML(tfutils.markdown(content.text));
let content_warning = html`
<div style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
`;
let content_html =
html`
@ -316,6 +342,8 @@ class TfMessageElement extends LitElement {
` :
content_warning :
content_html;
let is_encrypted = this.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
@ -331,9 +359,10 @@ class TfMessageElement extends LitElement {
display: block;
}
</style>
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span>${raw_button}</span>
@ -400,6 +429,11 @@ class TfMessageElement extends LitElement {
`;
} else if (content.type === 'pub') {
return small_frame(html`
<style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div>
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
@ -413,7 +447,14 @@ class TfMessageElement extends LitElement {
</div>
`);
} else if (typeof(this.message.content) == 'string') {
return small_frame(html`<span>🔒</span>`);
if (this.decrypted) {
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
} else if (this.decrypted === undefined) {
this.try_decrypt(content);
return small_frame(html`<span>🔐</span>`);
} else {
return small_frame(html`<span>🔒</span>`);
}
} else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
}

View File

@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
}
};
}
static styles = styles;
@ -145,9 +145,29 @@ class TfNewsElement extends LitElement {
return recursive_sort(roots, true);
}
async load_and_render(messages) {
group_following(messages) {
let result = [];
let group = [];
for (let message of messages) {
if (message?.content?.type === 'contact') {
group.push(message);
} else {
if (group.length > 0) {
result.push({
type: 'contact_group',
messages: group,
});
group = [];
}
result.push(message);
}
}
return result;
}
load_and_render(messages) {
let messages_by_id = this.process_messages(messages);
let final_messages = this.finalize_messages(messages_by_id);
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
return html`
<div style="display: flex; flex-direction: column">
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
@ -156,8 +176,7 @@ class TfNewsElement extends LitElement {
}
render() {
let messages = this.load_and_render(this.messages || []);
return html`${until(messages, html`<div>Loading placeholders...</div>`)}`;
return this.load_and_render(this.messages || []);
}
}

View File

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

View File

@ -29,4 +29,20 @@ img {
color: #088;
background-color: #fff;
}
.content_warning {
border: 1px solid #fff;
border-radius: 1em;
padding: 8px;
margin: 4px;
}
div.img_caption {
color: #888;
cursor: pointer;
}
div.img_caption::after {
content: ' ±';
}
`;

View File

@ -9,7 +9,7 @@ class TfTabConnectionsElement extends LitElement {
connections: {type: Array},
stored_connections: {type: Array},
users: {type: Object},
}
};
}
constructor() {
@ -71,7 +71,7 @@ class TfTabConnectionsElement extends LitElement {
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</li>
`
`;
}
async forget_stored_connection(connection) {

View File

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

View File

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

View File

@ -9,7 +9,7 @@ class TfTabSearchElement extends LitElement {
users: {type: Object},
following: {type: Array},
query: {type: String},
}
};
}
static styles = styles;

View File

@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
return {
id: {type: String},
users: {type: Object},
}
};
}
static styles = styles;
@ -19,18 +19,23 @@ class TfUserElement extends LitElement {
}
render() {
let name = this.users?.[this.id]?.name;
name = name !== undefined ?
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
if (this.users[this.id]) {
let image = this.users[this.id].image;
image = typeof(image) == 'string' ? image : image?.link;
return html`
<div style="display: inline-block; font-weight: bold">
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a>
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
${name}
</div>`;
} else {
return html`
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
<a target="_top" href=${'#' + this.id}>${this.id}</a>
<div style="display: inline-block; font-weight: bold">
${name}
</div>`;
}
}

93
apps/ssb/tf-utils.js Normal file
View File

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

4
apps/todo.json Normal file
View File

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

View File

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

View File

@ -1,7 +1,6 @@
import * as core from './core.js';
import * as form from './form.js';
let gTokens = {};
let gDatabase = new Database("auth");
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
@ -171,7 +170,7 @@ function handler(request, response) {
}
}
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; Secure; SameSite=Strict`;
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict`;
let entry = readSession(session);
if (entry && formData.return) {
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
@ -225,7 +224,7 @@ function handler(request, response) {
});
}
} else if (request.uri == "/login/logout") {
response.writeHead(303, {"Set-Cookie": "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT`, "Location": "/login" + (request.query ? "?" + request.query : "")});
response.end();
} else {
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});

View File

@ -1,10 +1,10 @@
import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';
let gSocket;
let gCredentials;
let gPermissions;
let gCurrentFile;
let gFiles = {};
let gApp = {files: {}};
let gApp = {files: {}, emoji: '📦'};
let gEditor;
let gSplit;
let gGraphs = {};
@ -26,6 +26,304 @@ const k_api = {
setHash: {args: ['hash'], func: api_setHash},
};
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
class TfNavigationElement extends LitElement {
static get properties() {
return {
credentials: {type: Object},
permissions: {type: Object},
show_permissions: {type: Boolean},
status: {type: Object},
spark_lines: {type: Object},
};
}
constructor() {
super();
this.permissions = {};
this.show_permissions = false;
this.status = {};
this.spark_lines = {};
}
toggle_edit(event) {
event.preventDefault();
if (editing()) {
closeEditor();
} else {
edit();
}
}
reset_permission(key) {
send({action: "resetPermission", permission: key});
}
get_spark_line(key, options) {
if (!this.spark_lines[key]) {
let spark_line = document.createElement('tf-sparkline');
spark_line.title = key;
if (options) {
if (options.max) {
spark_line.max = options.max;
}
}
this.spark_lines[key] = spark_line;
this.requestUpdate();
}
return this.spark_lines[key];
}
render_login() {
if (this?.credentials?.session?.name) {
return html`<a href="/login/logout?return=${url() + hash()}">logout ${this.credentials.session.name}</a>`;
} else {
return html`<a href="/login?return=${url() + hash()}">login</a>`;
}
}
render_permissions() {
if (this.show_permissions) {
return html`
<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
<div>This app has the following permissions:</div>
${Object.keys(this.permissions).map(key => html`
<div>
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
<button @click=${() => this.reset_permission(key)}>Reset</button>
</div>
`)}
<button @click=${() => this.show_permissions = false}>Close</button>
</div>
</div>
`;
}
}
render() {
let self = this;
return html`
<style>
${k_global_style}
</style>
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px">
<span>😎</span>
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
<span id="requests"></span>
${this.render_permissions()}
<span style="flex: 1; white-space: nowrap; overflow: hidden; margin: 0; padding: 0">${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [x.dataset.emoji, x])}</span>
<span style="flex: 0 0; white-space: nowrap">${this.render_login()}</span>
</div>
`;
}
}
customElements.define('tf-navigation', TfNavigationElement);
class TfFilesElement extends LitElement {
static get properties() {
return {
current: {type: String},
files: {type: Object},
};
}
constructor() {
super();
this.files = {};
}
file_click(file) {
this.dispatchEvent(new CustomEvent('file_click', {
detail: {
file: file,
},
bubbles: true,
composed: true,
}));
}
render_file(file) {
let classes = ['file'];
if (file == this.current) {
classes.push('current');
}
if (!this.files[file].clean) {
classes.push('dirty');
}
return html`<div class="${classes.join(' ')}" @click=${x => this.file_click(file)}>${file}</div>`;
}
render() {
let self = this;
return html`
<style>
div.file {
padding: 0.5em;
cursor: pointer;
}
div.file:hover {
background-color: #1a9188;
}
div.file::before {
content: '📄 ';
}
div.file.current {
font-weight: bold;
background-color: #2aa198;
}
div.file.dirty::after {
content: '*';
}
</style>
<div>
${Object.keys(this.files).sort().map(x => self.render_file(x))}
</div>
`;
}
}
customElements.define('tf-files', TfFilesElement);
class TfFilesPaneElement extends LitElement {
static get properties() {
return {
expanded: {type: Boolean},
current: {type: String},
files: {type: Object},
};
}
constructor() {
super();
this.expanded = window.localStorage.getItem('files') != '0';
this.files = {};
}
set_expanded(expanded) {
this.expanded = expanded;
window.localStorage.setItem('files', expanded ? '1' : '0');
}
render() {
let self = this;
let expander = this.expanded ?
html`<span @click=${() => self.set_expanded(false)} class="expander">«</span>` :
html`<span @click=${() => self.set_expanded(true)} class="expander">»</span>`;
let content = html`
<div id="files_content">
<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
<br>
<div><button @click=${() => newFile()}>New File</button></div>
<div><button @click=${() => removeFile()}>Remove File</button></div>
</div>
`;
return html`
<style>
.expander {
font-weight: bold;
width: 100%;
right: 0;
flex: 0;
padding: 0.25em;
cursor: pointer;
}
</style>
<div>
<div style="display: flex; flex-direction: row">
${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
${expander}
</div>
${this.expanded ? content : undefined}
</div>
`;
}
}
customElements.define('tf-files-pane', TfFilesPaneElement);
class TfSparkLineElement extends LitElement {
static get properties() {
return {
lines: {type: Array},
min: {type: Number},
max: {type: Number},
};
}
constructor() {
super();
this.min = 0;
this.max = 1.0;
this.lines = [];
this.k_values_max = 100;
}
append(key, value) {
let line = null;
for (let it of this.lines) {
if (it.name == key) {
line = it;
break;
}
}
if (!line) {
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
line = {
name: key,
style: k_colors[this.lines.length % k_colors.length],
values: Array(this.k_values_max).fill(0),
};
this.lines.push(line);
}
if (line.values.length >= this.k_values_max) {
line.values.shift();
}
line.values.push(value);
this.requestUpdate();
}
render_line(line) {
if (line?.values?.length >= 2) {
let max = Math.max(this.max, ...line.values);
let points = [].concat(...line.values.map((x, i) => [100.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (max - this.min)]));
return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`;
}
}
render() {
let max = Math.round(10.0 * Math.max(...this.lines.map(line => line.values[line.values.length - 1]))) / 10.0;
return html`
<svg style="width: 10em; height: 1.4em; vertical-align: top; margin: 0; padding: 0; background: #000" viewBox="0 0 100 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
${this.lines.map(x => this.render_line(x))}
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">${max}</text>
</svg>
`;
}
}
customElements.define('tf-sparkline', TfSparkLineElement);
window.addEventListener("keydown", function(event) {
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
if (editing()) {
@ -81,14 +379,6 @@ function editing() {
return document.getElementById("editPane").style.display != 'none';
}
function toggleEdit() {
if (editing()) {
closeEditor();
} else {
edit();
}
}
function edit() {
if (editing()) {
return;
@ -100,6 +390,7 @@ function edit() {
gSplit = undefined;
}
gSplit = Split(['#editPane', '#viewPane'], {minSize: 0});
document.getElementById("editPane").style.display = 'flex';
ensureLoaded([
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
@ -107,6 +398,7 @@ function edit() {
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}},
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
@ -118,6 +410,9 @@ function edit() {
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
{tagName: "script", attributes: {src: "/codemirror/lint.js"}},
{tagName: "script", attributes: {src: "/codemirror/jshint.js"}},
{tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}},
], function() {
load().catch(function(error) {
alert(error);
@ -126,30 +421,18 @@ function edit() {
});
}
function hideFiles() {
window.localStorage.setItem('files', '0');
document.getElementById('filesPane').classList.add('collapsed');
}
function showFiles() {
window.localStorage.setItem('files', '1');
document.getElementById('filesPane').classList.remove('collapsed');
}
function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}&title=Tilde%20Friends`);
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
}
function stats() {
window.localStorage.setItem('stats', '1');
document.getElementById("statsPane").style.display = 'flex';
send({action: 'enableStats', enabled: true});
}
function closeStats() {
window.localStorage.setItem('stats', '0');
document.getElementById("statsPane").style.display = 'none';
send({action: 'enableStats', enabled: false});
}
function toggleStats() {
@ -175,7 +458,6 @@ function loadFile(name, id) {
}).then(function(text) {
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
if (!Object.values(gFiles).some(x => !x.doc)) {
document.getElementById("editPane").style.display = 'flex';
openFile(Object.keys(gFiles).sort()[0]);
}
});
@ -200,6 +482,13 @@ function load(path) {
'indentUnit': 4,
'indentWithTabs': true,
'showTrailingSpace': true,
'gutters': ['CodeMirror-lint-markers'],
'mode': {'js': 'javascript'}[(path || url()).split('.').pop()],
'lint': {
'options': {
'esversion': 2021,
},
},
});
gEditor.on('changes', function() {
updateFiles();
@ -219,6 +508,8 @@ function load(path) {
document.getElementById("editPane").style.display = 'flex';
}
gApp = json;
gApp.emoji = gApp.emoji || '📦';
document.getElementById('icon').value = gApp.emoji;
}
if (!isApp) {
document.getElementById("editPane").style.display = 'flex';
@ -293,6 +584,7 @@ function save(save_to) {
let app = {
type: "tildefriends-app",
files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
emoji: gApp.emoji || '📦',
};
Object.values(gFiles).forEach(function(file) { delete file.id; });
gApp = JSON.parse(JSON.stringify(app));
@ -325,6 +617,14 @@ function save(save_to) {
});
}
function changeIcon() {
let value = prompt('Enter a new app icon emoji:');
if (value !== undefined) {
gApp.emoji = value || '📦';
document.getElementById('icon').value = gApp.emoji;
}
}
function deleteApp() {
let name = document.getElementById("name");
let path = name && name.value ? name.value : url();
@ -390,7 +690,8 @@ function api_localStorageGet(key) {
}
function api_requestPermission(permission, id) {
let permissions = document.getElementById('permissions');
let outer = document.createElement('div');
outer.classList.add('permissions');
let container = document.createElement('div');
container.classList.add('permissions_contents');
@ -434,17 +735,14 @@ function api_requestPermission(permission, id) {
button.innerText = option.text;
button.onclick = function() {
resolve(option.grant[check.checked ? 1 : 0]);
while (permissions.firstChild) {
permissions.removeChild(permissions.firstChild);
}
permissions.style.visibility = 'hidden';
document.body.removeChild(outer);
}
div.appendChild(button);
}
container.appendChild(div);
outer.appendChild(container);
permissions.appendChild(container);
permissions.style.visibility = 'visible';
document.body.appendChild(outer);
});
}
@ -456,85 +754,18 @@ function api_setHash(hash) {
window.location.hash = hash;
}
function hidePermissions() {
let permissions = document.getElementById('permissions_settings');
while (permissions.firstChild) {
permissions.removeChild(permissions.firstChild);
}
permissions.style.visibility = 'hidden';
}
function showPermissions() {
let permissions = document.getElementById('permissions_settings');
let container = document.createElement('div');
container.classList.add('permissions_contents');
let div = document.createElement('div');
div.appendChild(document.createTextNode('This app has the following permission:'));
for (let key of Object.keys(gPermissions || {})) {
let row = document.createElement('div');
let span = document.createElement('span');
span.appendChild(document.createTextNode(key));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(': '));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(gPermissions[key] ? '✅ Allowed' : '❌ Denied'));
row.appendChild(span);
span = document.createElement('span');
span.appendChild(document.createTextNode(' '));
row.appendChild(span);
let button = document.createElement('button');
button.innerText = 'Reset';
button.onclick = function() {
send({action: "resetPermission", permission: key});
};
row.appendChild(button);
div.appendChild(row);
}
container.appendChild(div);
div = document.createElement('div');
let button = document.createElement('button');
button.innerText = 'Close';
button.onclick = function() {
hidePermissions();
}
div.appendChild(button);
container.appendChild(div);
permissions.appendChild(container);
permissions.style.visibility = 'visible';
}
function _receive_websocket_message(message) {
if (message && message.action == "session") {
setStatusMessage("🟢 Executing...", kStatusColor);
gCredentials = message.credentials;
updateLogin();
document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials;
} else if (message && message.action == 'permissions') {
gPermissions = message.permissions;
let permissions = document.getElementById('permissions_settings');
if (permissions.firstChild) {
hidePermissions();
showPermissions();
}
document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {};
} else if (message && message.action == "ready") {
setStatusMessage(null);
if (window.location.hash) {
send({event: "hashChange", hash: window.location.hash});
}
if (window.localStorage.getItem('stats') == '1') {
/* Stats were opened before we connected. */
send({action: 'enableStats', enabled: true});
}
send({action: 'enableStats', enabled: true});
} else if (message && message.action == "ping") {
send({action: "pong"});
} else if (message && message.action == "stats") {
@ -544,6 +775,9 @@ function _receive_websocket_message(message) {
rpc_in: {group: 'rpc', name: 'in'},
rpc_out: {group: 'rpc', name: 'out'},
cpu_percent: {group: 'cpu', name: 'main'},
thread_percent: {group: 'cpu', name: 'work'},
arena_percent: {group: 'memory', name: 'm'},
js_malloc_percent: {group: 'memory', name: 'js'},
memory_percent: {group: 'memory', name: 'tot'},
@ -552,8 +786,8 @@ function _receive_websocket_message(message) {
tls_malloc_percent: {group: 'memory', name: 'tls'},
uv_malloc_percent: {group: 'memory', name: 'uv'},
messages_stored: {group: 'stored', name: 'messages'},
blobs_stored: {group: 'stored', name: 'blobs'},
messages_stored: {group: 'store', name: 'messages'},
blobs_stored: {group: 'store', name: 'blobs'},
socket_count: {group: 'socket', name: 'total'},
socket_open_count: {group: 'socket', name: 'open'},
@ -613,6 +847,16 @@ function _receive_websocket_message(message) {
}
}
timeseries.append(now, message.stats[key]);
if (graph_key == 'cpu' || graph_key == 'rpc' || graph_key == 'store') {
let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 });
line.dataset.emoji = {
'cpu': '💻',
'rpc': '🔁',
'store': '💾',
}[graph_key];
line.append(key, message.stats[key]);
}
}
} else if (message &&
message.message === 'tfrpc' &&
@ -638,27 +882,8 @@ function _receive_websocket_message(message) {
}
}
function keyEvent(event) {
send({
event: "key",
type: event.type,
which: event.which,
keyCode: event.keyCode,
charCode: event.charCode,
character: String.fromCharCode(event.keyCode || event.which),
altKey: event.altKey,
});
}
function setStatusMessage(message, color) {
let node = document.getElementById("status");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
if (message) {
node.appendChild(document.createTextNode(message));
node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor));
}
document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color};
}
function send(value) {
@ -671,23 +896,6 @@ function send(value) {
}
}
function updateLogin() {
let login = document.getElementById("login");
while (login.firstChild) {
login.removeChild(login.firstChild);
}
let a = document.createElement("a");
if (gCredentials && gCredentials.session) {
a.appendChild(document.createTextNode("logout " + gCredentials.session.name));
a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash()));
} else {
a.appendChild(document.createTextNode("login"));
a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash()));
}
login.appendChild(a);
}
function dragHover(event) {
event.stopPropagation();
event.preventDefault();
@ -828,10 +1036,12 @@ function message(event) {
function reconnect(path) {
let oldSocket = gSocket;
gSocket = null
oldSocket.onopen = null;
oldSocket.onclose = null;
oldSocket.onmessage = null;
oldSocket.close();
if (oldSocket) {
oldSocket.onopen = null;
oldSocket.onclose = null;
oldSocket.onmessage = null;
oldSocket.close();
}
connectSocket(path);
}
@ -895,27 +1105,13 @@ function openFile(name) {
gEditor.focus();
}
function onFileClicked(event) {
openFile(event.target.textContent);
}
function updateFiles() {
let node = document.getElementById("files");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
for (let file of Object.keys(gFiles).sort()) {
let li = document.createElement("li");
li.onclick = onFileClicked;
li.appendChild(document.createTextNode(file));
if (file == gCurrentFile) {
li.classList.add("current");
}
if (!gFiles[file].doc.isClean(gFiles[file].generation)) {
li.classList.add("dirty");
}
node.appendChild(li);
let files = document.getElementsByTagName("tf-files-pane")[0];
if (files) {
files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
clean: gFiles[file].doc.isClean(gFiles[file].generation),
}]));
files.current = gCurrentFile;
}
gEditor.focus();
@ -950,27 +1146,19 @@ window.addEventListener("load", function() {
window.addEventListener("message", message, false);
window.addEventListener("online", connectSocket);
document.getElementById("name").value = window.location.pathname;
document.getElementById('edit_link').addEventListener('click', function(event) {
event.preventDefault();
toggleEdit();
});
document.getElementById('show_permissions_link').addEventListener('click', () => showPermissions());
document.getElementById('files_hide').addEventListener('click', () => hideFiles());
document.getElementById('files_show').addEventListener('click', () => showFiles());
document.getElementById('closeStats').addEventListener('click', () => closeStats());
document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
document.getElementById('save').addEventListener('click', () => save());
document.getElementById('icon').addEventListener('click', () => changeIcon());
document.getElementById('delete').addEventListener('click', () => deleteApp());
document.getElementById('trace_button').addEventListener('click', function(event) {
event.preventDefault();
event.preventDefault();
trace();
});
document.getElementById('stats_button').addEventListener('click', function(event) {
event.preventDefault();
event.preventDefault();
toggleStats();
});
document.getElementById('new_file_button').addEventListener('click', () => newFile());
document.getElementById('remove_file_button').addEventListener('click', () => removeFile());
for (let tag of document.getElementsByTagName('a')) {
if (tag.accessKey) {
tag.classList.add('tooltip_parent');
@ -1003,11 +1191,6 @@ window.addEventListener("load", function() {
} else {
closeEditor();
}
if (window.localStorage.getItem('files') == '1') {
showFiles();
} else {
hideFiles();
}
if (window.localStorage.getItem('stats') == '1') {
stats();
} else {

View File

@ -1,11 +1,45 @@
import * as auth from './auth.js';
import * as app from './app.js';
import * as auth from './auth.js';
import * as form from './form.js';
import * as httpd from './httpd.js';
let gProcessIndex = 0;
let gProcesses = {};
let gStatsTimer = false;
const k_mime_types = {
'css': 'text/css',
'html': 'text/html',
'js': 'text/javascript',
'json': 'text/json',
'map': 'application/json',
'svg': 'image/svg+xml',
};
const k_magic_bytes = [
{bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'},
{bytes: [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01], type: 'image/jpeg'},
{bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'},
{bytes: [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00], type: 'image/jpeg'},
{bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'},
{bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'},
{bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'},
{bytes: [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], type: 'image/webp'},
{bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'},
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'audio/mpeg'},
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d], type: 'video/mp4'},
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'video/mp4'},
];
let k_static_files = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
];
const k_global_settings = {
index: {
type: 'string',
@ -38,8 +72,6 @@ let gGlobalSettings = {
index: "/~core/apps/",
};
let kGlobalSettingsFile = "data/global/settings.json";
let kPingInterval = 60 * 1000;
function printError(out, error) {
@ -47,8 +79,8 @@ function printError(out, error) {
out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
out.print(error.stackTrace);
} else {
for (let i in error) {
out.print(i);
for (let [k, v] of Object.entries(error)) {
out.print(k, v);
}
out.print(error.toString());
}
@ -145,12 +177,12 @@ async function getSessionProcessBlob(blobId, session, options) {
return getProcessBlob(blobId, 'session_' + session, actualOptions);
}
let gManifestCache = {};
async function getProcessBlob(blobId, key, options) {
let process = gProcesses[key];
if (!process
&& !(options && "create" in options && !options.create)) {
let resolveReady;
let rejectReady;
try {
print("Creating task for " + blobId + " " + key);
process = {};
@ -160,13 +192,13 @@ async function getProcessBlob(blobId, key, options) {
process.credentials = options.credentials || {};
process.task = new Task();
process.eventHandlers = {};
process.app = new app.App();
if (!options?.script || options?.script === 'app.js') {
process.app = new app.App();
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = options.timeout;
process.stats = false;
let resolveReady;
let rejectReady;
process.ready = new Promise(function(resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@ -246,7 +278,7 @@ async function getProcessBlob(blobId, key, options) {
throw Error(`Permission denied: ${permission}.`);
}
} else {
} else if (process.app) {
return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
if (value == 'allow') {
storePermission(user, options.packageOwner, options.packageName, permission, true);
@ -263,6 +295,8 @@ async function getProcessBlob(blobId, key, options) {
}
throw Error(`Permission denied: ${permission}.`);
});
} else {
throw Error(`Permission denied: ${permission}.`);
}
},
}
@ -313,14 +347,23 @@ async function getProcessBlob(blobId, key, options) {
imports.app[api[0]] = process.app.makeFunction(api);
}
}
for (let [name, f] of Object.entries(options?.imports || {})) {
imports[name] = f;
}
process.task.onPrint = function(args) {
imports.app.print(...args);
if (imports.app) {
imports.app.print(...args);
}
};
process.task.onError = function(error) {
try {
process.app.makeFunction(['error'])(error);
} catch(e) {
print(e);
if (process.app) {
process.app.makeFunction(['error'])(error);
} else {
printError({print: print}, error);
}
} catch (e) {
printError({print: print}, error);
}
};
imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
@ -347,6 +390,20 @@ async function getProcessBlob(blobId, key, options) {
});
}
};
imports.ssb.privateMessageEncrypt = function(id, recipients, message) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return ssb.privateMessageEncrypt(process.credentials.session.name, id, recipients, message);
}
};
imports.ssb.privateMessageDecrypt = function(id, message) {
if (process.credentials &&
process.credentials.session &&
process.credentials.session.name) {
return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
}
};
if (process.credentials &&
process.credentials.session &&
@ -385,7 +442,7 @@ async function getProcessBlob(blobId, key, options) {
try {
let appObject = JSON.parse(appSource);
if (appObject.type == "tildefriends-app") {
appSourceName = 'app.js';
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await getBlobOrContent(id);
appSource = utf8Decode(blob);
@ -398,19 +455,23 @@ async function getProcessBlob(blobId, key, options) {
printError({print: print}, e);
}
broadcastEvent('onSessionBegin', [getUser(process, process)]);
resolveReady(process);
if (process.app) {
process.app.send({action: "ready"});
process.sendPermissions();
}
await process.task.execute({name: appSourceName, source: appSource});
resolveReady(process);
} catch (error) {
if (process?.task?.onError) {
process.task.onError(error);
if (process.app) {
if (process?.task?.onError) {
process.task.onError(error);
} else {
printError({print: print}, error);
}
} else {
printError({print: print}, error);
}
rejectReady();
rejectReady(error);
}
}
return process;
@ -425,20 +486,11 @@ function setGlobalSettings(settings) {
}
}
let kStaticFiles = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', type: 'image/png'},
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
];
function startsWithBytes(data, bytes) {
if (data.byteLength >= bytes.length) {
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
for (let i = 0; i < bytes.length; i++) {
if (dataBytes[i] != bytes[i] && bytes[i] !== null) {
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
return;
}
}
@ -447,16 +499,16 @@ function startsWithBytes(data, bytes) {
}
async function staticFileHandler(request, response, blobId, uri) {
for (let i in kStaticFiles) {
if (uri === kStaticFiles[i].uri) {
let path = kStaticFiles[i].path || uri.substring(1);
let type = kStaticFiles[i].type || guessType(path);
for (let i in k_static_files) {
if (uri === k_static_files[i].uri) {
let path = k_static_files[i].path || uri.substring(1);
let type = k_static_files[i].type || guessTypeFromName(path);
let stat = await File.stat('core/' + path);
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile('core/' + path);
@ -466,7 +518,7 @@ async function staticFileHandler(request, response, blobId, uri) {
'Content-Length': data.byteLength,
'etag': '"' + id + '"',
},
kStaticFiles[i].headers || {}));
k_static_files[i].headers || {}));
response.end(data);
}
return;
@ -477,14 +529,6 @@ async function staticFileHandler(request, response, blobId, uri) {
response.end("File not found");
}
const k_mime_types = {
'json': 'text/json',
'js': 'text/javascript',
'html': 'text/html',
'css': 'text/css',
'map': 'application/json',
};
async function staticDirectoryHandler(request, response, directory, uri) {
let filename = uri || 'index.html';
if (filename.indexOf('..') != -1) {
@ -498,7 +542,7 @@ async function staticDirectoryHandler(request, response, directory, uri) {
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile(directory + filename);
@ -526,35 +570,23 @@ async function wellKnownHandler(request, response, path) {
}
}
function guessTypeFromName(path) {
let extension = path.split('.').pop();
return k_mime_types[extension];
}
function guessTypeFromMagicBytes(data) {
for (let magic of k_magic_bytes) {
if (startsWithBytes(data, magic.bytes)) {
return magic.type;
}
}
}
function sendData(response, data, type, headers) {
if (data) {
if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) ||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
response.writeHead(200, Object.assign({"Content-Type": "image/jpeg", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
response.writeHead(200, Object.assign({"Content-Type": "image/png", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
response.writeHead(200, Object.assign({"Content-Type": "image/gif", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
response.writeHead(200, Object.assign({"Content-Type": "image/webp", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
response.writeHead(200, Object.assign({"Content-Type": "audio/mpeg", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
response.writeHead(200, Object.assign({"Content-Type": "video/mp4", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else {
response.writeHead(200, Object.assign({"Content-Type": type || "application/binary", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
}
response.writeHead(200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {}));
response.end(data);
} else {
response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
response.end("File not found");
@ -571,35 +603,53 @@ async function getBlobOrContent(id) {
}
}
function guessType(path) {
const k_extension_to_type = {
'css': 'text/css',
'html': 'text/html',
'js': 'text/javascript',
'svg': 'image/svg+xml',
};
let extension = path.split('.').pop();
return k_extension_to_type[extension];
let g_handler_index = 0;
async function useAppHandler(response, handler_blob_id, path) {
let do_resolve;
let promise = new Promise(async function(resolve, reject) {
do_resolve = resolve;
});
let process;
let result;
try {
process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, {
script: 'handler.js',
imports: {
request: {
path: path,
},
respond: do_resolve,
},
});
await process.ready;
result = await promise;
} finally {
if (process?.task) {
await process.task.kill();
}
}
return result;
}
async function blobHandler(request, response, blobId, uri) {
for (let i in kStaticFiles) {
if (uri === kStaticFiles[i].uri && kStaticFiles[i].path) {
let stat = await File.stat('core/' + kStaticFiles[i].path);
for (let i in k_static_files) {
if (uri === k_static_files[i].uri && k_static_files[i].path) {
let stat = await File.stat('core/' + k_static_files[i].path);
let id = `${stat.mtime}_${stat.size}`;
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
response.writeHead(304, {'Content-Length': '0'});
response.end();
} else {
let data = await File.readFile('core/' + kStaticFiles[i].path);
let data = await File.readFile('core/' + k_static_files[i].path);
response.writeHead(200, Object.assign(
{
'Content-Type': kStaticFiles[i].type,
'Content-Type': k_static_files[i].type,
'Content-Length': data.byteLength,
'etag': '"' + id + '"',
},
kStaticFiles[i].headers || {}));
k_static_files[i].headers || {}));
response.end(data);
}
return;
@ -616,11 +666,19 @@ async function blobHandler(request, response, blobId, uri) {
if (uri == "/view") {
let data;
let match;
let query = form.decodeForm(request.query);
let headers = {
'Content-Security-Policy': 'sandbox',
};
if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
}
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
let id = await new Database(match[1]).get('path:' + match[2]);
if (id) {
if (request.headers['if-none-match'] === '"' + id + '"') {
response.writeHead(304, {});
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(id);
@ -628,23 +686,25 @@ async function blobHandler(request, response, blobId, uri) {
let appObject = JSON.parse(data);
data = appObject.files[match[3]];
}
sendData(response, data, undefined, {etag: '"' + id + '"'});
sendData(response, data, undefined, Object.assign({etag: '"' + id + '"'}, headers));
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
response.writeHead(304, {});
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
}
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
response.writeHead(304, {});
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
data = await getBlobOrContent(blobId);
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
}
}
} else if (uri == "/save") {
@ -705,7 +765,7 @@ async function blobHandler(request, response, blobId, uri) {
} catch {
}
if (apps.delete(appName)) {
database.set('apps', JSON.stringify([...apps]));
database.set('apps', JSON.stringify([...apps].sort()));
}
database.remove('path:' + appName);
} else {
@ -719,43 +779,54 @@ async function blobHandler(request, response, blobId, uri) {
response.end('OK');
} else {
let data;
let type;
let headers;
let match;
let id;
let app_id = blobId;
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
let db = new Database(match[1]);
let id = await db.get('path:' + match[2]);
if (id) {
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
headers = {
'Access-Control-Allow-Origin': '*',
};
response.writeHead(304, headers);
response.end();
} else {
data = utf8Decode(await getBlobOrContent(id));
let appObject = JSON.parse(data);
data = appObject.files[uri.substring(1)];
data = await getBlobOrContent(data);
type = guessType(uri);
headers = {
'ETag': '"' + id + '"',
'Access-Control-Allow-Origin': '*',
};
sendData(response, data, type, headers);
}
app_id = await db.get('path:' + match[2]);
}
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
id = app_object.files[uri.substring(1)];
if (!id && app_object.files['handler.js']) {
let answer;
try {
answer = await useAppHandler(response, app_id, uri.substring(1));
} catch (error) {
data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`);
response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length});
response.end(data);
return;
}
if (answer && typeof answer.data == 'string') {
answer.data = utf8Encode(answer.data);
}
sendData(response, answer?.data, answer?.content_type, {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
});
} else if (id) {
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
let headers = {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
'Content-Length': '0',
};
response.writeHead(304, headers);
response.end();
} else {
let headers = {
'ETag': '"' + id + '"',
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': 'sandbox',
};
data = await getBlobOrContent(id);
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
sendData(response, data, type, headers);
}
} else {
data = utf8Decode(await getBlobOrContent(blobId));
let appObject = JSON.parse(data);
data = appObject.files[uri.substring(1)];
data = await getBlobOrContent(data);
headers = {
'Access-Control-Allow-Origin': '*',
};
sendData(response, data, type, headers);
sendData(response, data, undefined, {});
}
}
}
@ -769,8 +840,7 @@ ssb.addEventListener('connections', function() {
});
async function loadSettings() {
let data;
let data = {};
try {
let settings = new Database('core').get('settings');
if (settings) {
@ -779,19 +849,12 @@ async function loadSettings() {
} catch (error) {
print("Settings not found in database:", error);
}
if (!data) {
try {
data = JSON.parse(utf8Decode(await File.readFile(kGlobalSettingsFile)));
new Database('core').set('settings', JSON.stringify(data));
} catch (error) {
print("Unable to load settings from " + kGlobalSettingsFile + ":", error);
for (let [key, value] of Object.entries(k_global_settings)) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
}
if (data) {
gGlobalSettings = data;
}
gGlobalSettings = data;
}
function sendStats() {
@ -815,6 +878,16 @@ function enableStats(process, enabled) {
}
}
function stringResponse(response, data) {
let bytes = utf8Encode(data);
response.writeHead(200, {
"Content-Type": "application/json; charset=utf-8",
"Content-Length": bytes.byteLength.toString(),
"Access-Control-Allow-Origin": "*",
});
return response.end(bytes);
}
loadSettings().then(function() {
httpd.all("/login", auth.handler);
httpd.all("", function(request, response) {
@ -826,8 +899,8 @@ loadSettings().then(function() {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
return staticFileHandler(request, response, null, match[1]);
} else if (match = /^\/static\/lit\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/lit/', match[1]);
} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
@ -836,22 +909,22 @@ loadSettings().then(function() {
return staticDirectoryHandler(request, response, 'deps/split/', match[1]);
} else if (match = /^\/smoothie\/([\.\w-/]*)$/.exec(request.uri)) {
return staticDirectoryHandler(request, response, 'deps/smoothie/', match[1]);
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
return staticFileHandler(request, response, null, match[1]);
} else if (request.uri == "/robots.txt") {
return staticFileHandler(request, response, null, request.uri);
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/trace$/.exec(request.uri)) {
let data = trace();
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
return stringResponse(response, trace());
} else if (match = /^\/disconnections$/.exec(request.uri)) {
let data = utf8Encode(JSON.stringify(disconnectionsDebug(), null, 2));
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.byteLength.toString()});
return response.end(data);
return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
} else if (match = /^\/debug$/.exec(request.uri)) {
let data = JSON.stringify(getDebug(), null, 2);
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
} else if (request.uri == "/robots.txt") {
return blobHandler(request, response, null, request.uri);
return stringResponse(response, JSON.stringify(getDebug(), null, 2));
} else if (match = /^\/hitches$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getHitches(), null, 2));
} else if (match = /^\/mem$/.exec(request.uri)) {
return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
return wellKnownHandler(request, response, match[1]);
} else {

View File

@ -1,7 +1,7 @@
function decode(encoded) {
var result = "";
for (var i = 0; i < encoded.length; i++) {
var c = encoded[i];
let result = "";
for (let i = 0; i < encoded.length; i++) {
let c = encoded[i];
if (c == "+") {
result += " ";
} else if (c == "%") {
@ -15,15 +15,15 @@ function decode(encoded) {
}
function decodeForm(encoded, initial) {
var result = initial || {};
let result = initial || {};
if (encoded) {
encoded = encoded.trim();
var items = encoded.split('&');
for (var i = 0; i < items.length; i++) {
var item = items[i];
var equals = item.indexOf('=');
var key = decode(item.slice(0, equals));
var value = decode(item.slice(equals + 1));
let items = encoded.split('&');
for (let i = 0; i < items.length; i++) {
let item = items[i];
let equals = item.indexOf('=');
let key = decode(item.slice(0, equals));
let value = decode(item.slice(equals + 1));
result[key] = value;
}
}

View File

@ -456,8 +456,6 @@ function handleConnection(client) {
}
}
client.noDelay = true;
client.onError(function(error) {
logError(client.peerName + " - - [" + new Date() + "] " + error);
});
@ -495,7 +493,7 @@ function handleConnection(client) {
parsing_header = false;
inputBuffer = inputBuffer.slice(result.bytes_parsed);
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect) {
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) {
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
@ -557,11 +555,25 @@ let kBacklog = 8;
let kHost = "0.0.0.0"
let socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function() {
let listenResult = socket.listen(kBacklog, function() {
socket.accept().then(handleConnection).catch(function(error) {
logError("[" + new Date() + "] accept error " + error);
socket.bind(kHost, tildefriends.http_port).then(function(port) {
print("bound to", port);
print("checking", tildefriends.args.out_http_port_file);
if (tildefriends.args.out_http_port_file) {
print("going to write the file");
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
print("wrote port file", tildefriends.args.out_http_port_file, r);
}).catch(function() {
print("failed to write port file");
});
}
let listenResult = socket.listen(kBacklog, async function() {
try {
let client = await socket.accept();
client.noDelay = true;
handleConnection(client);
} catch (error) {
logError("[" + new Date() + "] accept error " + error);
}
});
}).catch(function(error) {
logError("[" + new Date() + "] bind error " + error);
@ -574,6 +586,7 @@ if (tildefriends.https_port) {
return secureSocket.listen(kBacklog, async function() {
try {
let client = await secureSocket.accept();
client.noDelay = true;
client.tls = true;
const kCertificatePath = "data/httpd/certificate.pem";
const kPrivateKeyPath = "data/httpd/privatekey.pem";

View File

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

View File

@ -15,11 +15,6 @@ body {
margin: 0;
}
.navigation {
height: auto;
margin: 4px;
}
a:link {
color: #268bd2;
}
@ -162,70 +157,6 @@ a:active {
.cyan { color: #2aa198; }
.green { color: #859900; }
#files_header {
font-weight: bold;
text-align: center;
flex: 1;
}
#files_hide {
font-weight: bold;
width: 100%;
right: 0;
flex: 0;
padding: 0.25em;
cursor: pointer;
}
#files_show {
display: none;
padding: 0.25em;
cursor: pointer;
}
#filesPane {
flex: 1 1 10%;
}
#filesPane.collapsed {
flex: 0;
}
.collapsed #files_header {
display: none;
}
.collapsed #files_content {
display: none;
}
.collapsed #files_hide {
display: none;
}
.collapsed #files_show {
display: block;
}
#files {
list-style-type: none;
margin: 0;
padding: 0;
}
#files > li {
padding: 0.5em;
}
#files > li.current {
font-weight: bold;
background-color: #2aa198;
}
#files > li.dirty::after {
content: '*';
}
.tooltip {
position: absolute;
z-index: 1;
@ -254,8 +185,7 @@ kbd {
white-space: nowrap;
}
#permissions, #permissions_settings {
visibility: hidden;
.permissions {
position: absolute;
display: block;
top: 0;

View File

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

View File

@ -1,106 +0,0 @@
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# http://www.gnu.org/software/automake
Makefile
Makefile.in
/ar-lib
/mdate-sh
/py-compile
/test-driver
/ylwrap
# http://www.gnu.org/software/autoheader
config.h
# http://www.gnu.org/software/autoconf
autom4te.cache
/autoscan.log
/autoscan-*.log
/aclocal.m4
/compile
/config.guess
/config.h.in
/config.log
/config.status
/config.sub
/configure
/configure.scan
/depcomp
/install-sh
/missing
/stamp-h1
# https://www.gnu.org/software/libtool/
/ltmain.sh
# http://www.gnu.org/software/texinfo
/texinfo.tex
# http://www.gnu.org/software/m4/
m4/libtool.m4
m4/ltoptions.m4
m4/ltsugar.m4
m4/ltversion.m4
m4/lt~obsolete.m4
# vim
*.swp
# project specific
test/gen
test/test[0-9]*
test/.deps

29
deps/base64c/LICENSE vendored
View File

@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2018, Sean Hanna
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,2 +0,0 @@
AUTOMAKE_OPTIONS = foreign
SUBDIRS = src test

View File

@ -1,60 +0,0 @@
# base64c
This is primarily just a fork of a base64 decoder from the FreeBSD codebase. It has received a few modifications:
* removed all allocations, you are expected to pass in a buffer that has sufficient space and you will get an error (-1) if you run out of space
* replaced a dynamically generated lookup table with a hardcoded lookup table
* wrote my own unit tests, i'm sure there are tests for freebsd somewhere but i didn't find them
# Embedding
This code is primarily intended to be dropped into an existing code base ( or perhaps using submodules). To do that:
* grab include/base64c.h
* grab src/base64c.h
# Usage
Call base64c_encoding_length() to calculate how big a buffer you need to encode a string. It's somewhere around 4 times the size of the input string. This length includes a null terminator.
```c
char input_string[256];
size_t new_len = base64c_encoding_length( strlen(input_string));
unsigned char *buffer = (unsigned char*)malloc(new_len);
```
Call base64c_encode() to actually encode your input string as base64. It will write to the buffer and return how many characters were written. If there was an error it will return -1.
```c
size_t output_length = base64c_encode(input_string, strlen(input_string), buffer, new_len);
if (output_length == -1) {
int x = 1/0; // ERROR!
}
```
Call base64c_decoding_length() to calculate how big a buffer you need to decode. It comes out to about half the size. This number isn't always exact, but it is close to within a byte or two.
```c
size_t decode_len = base64c_decoding_length( strlen(buffer) );
unsigned char *decoded = (unsigned char*)malloc( decode_len );
```
Call base64c_decode() to decode an encoded base64 string. It will write to the buffer and return how many characters were written. IF there was an error it will return -1. If the string contains invalid number of characters, or has any characters that are not part of the base64 character set an error will be returned.
# Building
You need to bootstrap all the autoconf tools by running ./autogen.sh
You need to have autoconf installed to do this.
Once bootstrapped run ./configure
# Tests
There are tests in the test/ subfolder. They will be built automatically. There is no special test runner. You can run each of the test cases manually to check whether the code is working properly.
# References
(http://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c)
(https://github.com/freebsd/freebsd/blob/master/contrib/wpa/src/utils/base64.c)

View File

@ -1,3 +0,0 @@
#!/bin/sh
aclocal && automake --gnu --add-missing && autoconf

View File

@ -1,22 +0,0 @@
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
BASE64C_VERSION=0.5
AC_PREREQ([2.69])
AC_INIT(base64c, 0.5, hannasm@gmail.com)
AM_INIT_AUTOMAKE(base64c, 0.5)
AC_CONFIG_SRCDIR([include/base64c.h])
AC_CONFIG_HEADERS([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
AC_TYPE_SIZE_T
# Checks for library functions.
AC_OUTPUT(Makefile src/Makefile test/Makefile)

View File

@ -1,42 +0,0 @@
#ifndef base64cC_H
#define base64cC_H
#include <stddef.h>
#include <stdint.h>
/**
* base64c_encoding_length - calculate length to allocate for encode
* @len: Length of input string
* Returns: number of bytes required to base64c encode, this includes room for '\0' terminator
*/
size_t base64c_encoding_length(size_t len);
/**
* base64c_decoding_length - calculate length to allocate for decode
* @len: Length of (base64 encoded) input string
* Returns: maximum number of bytes required to decode
*/
size_t base64c_decoding_length(size_t inlen);
/**
* base64c_encode - base64c encode
* @src: Data to be encoded
* @len: Length of the data to be encoded
* @out: Mutable output buffer destination, all encoded bytes will be written to the destination
* @out_len: length of output buffer
* Returns: number of bytes written, or -1 if there was an error
*/
size_t base64c_encode(const unsigned char *src, size_t len, unsigned char* out, const size_t out_len);
/**
* base64c_decode - base64c decode
* @src: Data to be decoded
* @len: Length of the data to be decoded
* @out_len: Pointer to output length variable
* Returns: Allocated buffer of out_len bytes of decoded data,
* or %NULL on failure
*
* Caller is responsible for freeing the returned buffer.
*/
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len);
#endif

View File

@ -1,3 +0,0 @@
CFLAGS = --pednatic -Wall -stdc99 -O2
LDFLAGS =

View File

@ -1,139 +0,0 @@
#include <string.h>
#include <stdio.h>
/*
* Base64 encoding/decoding (RFC1341)
* Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
*
* This software may be distributed under the terms of the BSD license.
* See README for more details.
*/
static const unsigned char base64c_table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const unsigned char base64c_dtable[256] = {
/*000*/0x80,/*001*/0x80,/*002*/0x80,/*003*/0x80,/*004*/0x80,/*005*/0x80,/*006*/0x80,/*007*/0x80,/*008*/0x80,/*009*/0x80,/*010*/0x80,/*011*/0x80,/*012*/0x80,/*013*/0x80,/*014*/0x80,/*015*/0x80,/*016*/0x80,/*017*/0x80,/*018*/0x80,/*019*/0x80,
/*020*/0x80,/*021*/0x80,/*022*/0x80,/*023*/0x80,/*024*/0x80,/*025*/0x80,/*026*/0x80,/*027*/0x80,/*028*/0x80,/*029*/0x80,/*030*/0x80,/*031*/0x80,/*032*/0x80,/*033*/0x80,/*034*/0x80,/*035*/0x80,/*036*/0x80,/*037*/0x80,/*038*/0x80,/*039*/0x80,
/*040*/0x80,/*041*/0x80,/*042*/0x80,/*043*/0x3e,/*044*/0x80,/*045*/0x80,/*046*/0x80,/*047*/0x3f,/*048*/0x34,/*049*/0x35,/*050*/0x36,/*051*/0x37,/*052*/0x38,/*053*/0x39,/*054*/0x3a,/*055*/0x3b,/*056*/0x3c,/*057*/0x3d,/*058*/0x80,/*059*/0x80,
/*060*/0x80,/*061*/0x00,/*062*/0x80,/*063*/0x80,/*064*/0x80,/*065*/0x00,/*066*/0x01,/*067*/0x02,/*068*/0x03,/*069*/0x04,/*070*/0x05,/*071*/0x06,/*072*/0x07,/*073*/0x08,/*074*/0x09,/*075*/0x0a,/*076*/0x0b,/*077*/0x0c,/*078*/0x0d,/*079*/0x0e,
/*080*/0x0f,/*081*/0x10,/*082*/0x11,/*083*/0x12,/*084*/0x13,/*085*/0x14,/*086*/0x15,/*087*/0x16,/*088*/0x17,/*089*/0x18,/*090*/0x19,/*091*/0x80,/*092*/0x80,/*093*/0x80,/*094*/0x80,/*095*/0x80,/*096*/0x80,/*097*/0x1a,/*098*/0x1b,/*099*/0x1c,
/*100*/0x1d,/*101*/0x1e,/*102*/0x1f,/*103*/0x20,/*104*/0x21,/*105*/0x22,/*106*/0x23,/*107*/0x24,/*108*/0x25,/*109*/0x26,/*110*/0x27,/*111*/0x28,/*112*/0x29,/*113*/0x2a,/*114*/0x2b,/*115*/0x2c,/*116*/0x2d,/*117*/0x2e,/*118*/0x2f,/*119*/0x30,
/*120*/0x31,/*121*/0x32,/*122*/0x33,/*123*/0x80,/*124*/0x80,/*125*/0x80,/*126*/0x80,/*127*/0x80,/*128*/0x80,/*129*/0x80,/*130*/0x80,/*131*/0x80,/*132*/0x80,/*133*/0x80,/*134*/0x80,/*135*/0x80,/*136*/0x80,/*137*/0x80,/*138*/0x80,/*139*/0x80,
/*140*/0x80,/*141*/0x80,/*142*/0x80,/*143*/0x80,/*144*/0x80,/*145*/0x80,/*146*/0x80,/*147*/0x80,/*148*/0x80,/*149*/0x80,/*150*/0x80,/*151*/0x80,/*152*/0x80,/*153*/0x80,/*154*/0x80,/*155*/0x80,/*156*/0x80,/*157*/0x80,/*158*/0x80,/*159*/0x80,
/*160*/0x80,/*161*/0x80,/*162*/0x80,/*163*/0x80,/*164*/0x80,/*165*/0x80,/*166*/0x80,/*167*/0x80,/*168*/0x80,/*169*/0x80,/*170*/0x80,/*171*/0x80,/*172*/0x80,/*173*/0x80,/*174*/0x80,/*175*/0x80,/*176*/0x80,/*177*/0x80,/*178*/0x80,/*179*/0x80,
/*180*/0x80,/*181*/0x80,/*182*/0x80,/*183*/0x80,/*184*/0x80,/*185*/0x80,/*186*/0x80,/*187*/0x80,/*188*/0x80,/*189*/0x80,/*190*/0x80,/*191*/0x80,/*192*/0x80,/*193*/0x80,/*194*/0x80,/*195*/0x80,/*196*/0x80,/*197*/0x80,/*198*/0x80,/*199*/0x80,
/*200*/0x80,/*201*/0x80,/*202*/0x80,/*203*/0x80,/*204*/0x80,/*205*/0x80,/*206*/0x80,/*207*/0x80,/*208*/0x80,/*209*/0x80,/*210*/0x80,/*211*/0x80,/*212*/0x80,/*213*/0x80,/*214*/0x80,/*215*/0x80,/*216*/0x80,/*217*/0x80,/*218*/0x80,/*219*/0x80,
/*220*/0x80,/*221*/0x80,/*222*/0x80,/*223*/0x80,/*224*/0x80,/*225*/0x80,/*226*/0x80,/*227*/0x80,/*228*/0x80,/*229*/0x80,/*230*/0x80,/*231*/0x80,/*232*/0x80,/*233*/0x80,/*234*/0x80,/*235*/0x80,/*236*/0x80,/*237*/0x80,/*238*/0x80,/*239*/0x80,
/*240*/0x80,/*241*/0x80,/*242*/0x80,/*243*/0x80,/*244*/0x80,/*245*/0x80,/*246*/0x80,/*247*/0x80,/*248*/0x80,/*249*/0x80,/*250*/0x80,/*251*/0x80,/*252*/0x80,/*253*/0x80,/*254*/0x80,/*255*/0x00,
};
size_t base64c_encoding_length(size_t len) {
size_t olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
olen++; /* nul termination */
if (olen < len)
return 0; /* integer overflow */
return olen;
}
size_t base64c_encode(const unsigned char *src, size_t len,
unsigned char* out, const size_t out_len)
{
unsigned char *pos;
const unsigned char *end, *in;
const unsigned char *out_end = out + out_len;
end = src + len;
in = src;
pos = out;
if (out_len < base64c_encoding_length(len)) { return -1; }
while (end - in >= 3 ) {
*pos++ = base64c_table[in[0] >> 2];
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64c_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
*pos++ = base64c_table[in[2] & 0x3f];
in += 3;
}
if (end - in) {
*pos++ = base64c_table[in[0] >> 2];
if (end - in == 1) {
*pos++ = base64c_table[(in[0] & 0x03) << 4];
*pos++ = '=';
} else {
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64c_table[(in[1] & 0x0f) << 2];
}
*pos++ = '=';
}
*pos = '\0';
return out_len - (out_end-pos);
}
size_t base64c_decoding_length(size_t inlen) {
return inlen / 4 * 3;
}
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len)
{
if (out == NULL) { return 0; }
if (out_len <= 0) { return 0; }
unsigned char *pos, block[4], tmp;
size_t i, count;
int pad = 0;
if (len == 0 ){
*out = '\0';
return 1;
}
if (len % 4) {
return -1;
}
pos = out;
count = 0;
for (i = 0; i < len; i++) {
if (src[i] == '=') { pad++; }
tmp = base64c_dtable[src[i]];
if (tmp == 0x80) { return -1; }
block[count] = tmp;
count++;
if (count == 4) {
switch (pad) {
case 0:
if ((pos - out) + 3 > out_len) {
return -1;
}
*pos++ = (block[0] << 2) | (block[1] >> 4);
*pos++ = (block[1] << 4) | (block[2] >> 2);
*pos++ = (block[2] << 6) | block[3];
break;
case 1:
if ((pos - out) + 2 > out_len || i + 1 > len) {
return -1;
}
*pos++ = (block[0] << 2) | (block[1] >> 4);
*pos++ = (block[1] << 4) | (block[2] >> 2);
break;
case 2:
if ((pos - out) + 1 > out_len || i + 1 > len) {
return -1;
}
*pos++ = (block[0] << 2) | (block[1] >> 4);
break;
default:
break;
}
count = 0;
}
}
return pos - out;
}

View File

@ -1,16 +0,0 @@
CFLAGS = --pedantic -Wall -std=c99 -g -ggdb
LDFLAGS =
bin_PROGRAMS = test001 test002 test003 test004 \
test005 test006 test007 test008 \
gen
test001_SOURCES = test001.c ../src/base64c.c
test002_SOURCES = test002.c ../src/base64c.c
test003_SOURCES = test003.c ../src/base64c.c
test004_SOURCES = test004.c ../src/base64c.c
test005_SOURCES = test005.c ../src/base64c.c
test006_SOURCES = test006.c ../src/base64c.c
test007_SOURCES = test007.c ../src/base64c.c
test008_SOURCES = test008.c ../src/base64c.c
gen_SOURCES = gen.c

View File

@ -1,22 +0,0 @@
#include <stdio.h>
#include <string.h>
static const unsigned char base64_table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int main() {
unsigned char out[256];
memset(out, 0x80, 255);
for (int i = 0; i < 64; i++) {
out[base64_table[i]] = i;
}
out['='] = 0;
printf("static const unsigned char base64c_dtable[256] = {");
for (int i = 0; i < 256; i++) {
if (i% 20==0) { printf("\n"); }
printf("/*%03d*/0x%02x,", i, out[i]);
}
printf("\n};");
}

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