Compare commits

..

100 Commits

Author SHA1 Message Date
3c0b680b8e Let's release 0.0.17. 2024-03-27 18:59:40 -04:00
895356897b archive whichever branch. 2024-03-25 18:10:07 -04:00
9164be2f37 Fix loading from not standalone zip. 2024-03-25 16:34:27 -04:00
5385264f94 Fix an http use after free during shutdown. 2024-03-25 16:31:09 -04:00
610e756c07 Ever closer to the elusive clean http shutdown. 2024-03-25 16:23:45 -04:00
15c9f8f458 Rudimentary support for building the executable with data attached. Pushed some things around in the makefile to fix issues along the way. #46 2024-03-25 13:50:17 -04:00
fb704a5b83 I will get better at keeping this tree clean. 2024-03-25 12:56:33 -04:00
fdda628be8 Fix paths in the source tarball. 2024-03-20 20:43:41 -04:00
2b45d8aa05 Doh. Never mean to add that. 2024-03-20 20:37:52 -04:00
0e2fc65301 Document run -k flag. 2024-03-20 20:33:23 -04:00
e8ef7e74de Fixed a leak in JS blob store. 2024-03-18 12:46:12 -04:00
c32e1b9583 http request cleanup crash fix. 2024-03-18 16:34:07 +00:00
9d0f6ec155 Fix the sneaker app RE: JSONB. 2024-03-18 12:32:40 -04:00
855d603795 docs + prettier 2024-03-17 13:21:33 -04:00
af25782185 More http/request shutdown issues. 2024-03-17 12:38:37 -04:00
e5ba51b80a Chasing a leak that looks like an EBT clock. Deleted some unneeded code and adding a missing JS free. 2024-03-17 13:44:05 +00:00
5e240de677 Fix requesting blobs from blob_wants. ids were trucated. Yikes. 2024-03-17 09:16:06 -04:00
418cfac0e3 Add a stock app with local room connection info. #15 2024-03-14 00:43:11 +00:00
9d09607013 Update CodeMirror. 2024-03-13 20:23:48 -04:00
eddf25b622 Give libuv the same download treatment as sqlite. 2024-03-13 19:53:57 -04:00
537a8654fa Rename sequence_before_author => flags. #29 2024-03-13 19:40:09 -04:00
9de33d06d2 Specifying -fsanitize=... early seems good. 2024-03-13 18:26:24 -04:00
0e5f320664 sqlite 3.45.2. 2024-03-13 12:30:14 -04:00
88d8e60511 Some minor paranoia to appease valgrind. 2024-03-12 21:44:20 -04:00
439f07162e Disable Haiku automation tests until I find a way to automate a browser on Haiku. 2024-03-09 08:44:06 -05:00
efe2b6cbd9 A make target to run prettier. 2024-03-08 21:43:08 -05:00
0aa1ed9464 Fix a failure requesting more blobs. 2024-03-08 21:38:31 -05:00
cb94ed6a2a Some plumbing to expose the actual bound SHS port so that I can make a dynamic room app. 2024-03-07 21:03:14 -05:00
cf187ee46b Reorder things so that we only zipalign -z during a dist build. To slow for make all. 2024-03-07 20:42:08 -05:00
3e71fc20fd Prettier. 2024-03-06 21:14:09 -05:00
f3601321f7 That's all the doxygen warnings. #27 2024-03-06 21:13:16 -05:00
540059368c 11 make docs warnings left, but I'm out of time for tonight. 2024-03-06 20:57:38 -05:00
7ce89123f7 85 make docs warnings remain. 2024-03-06 12:46:27 -05:00
e3c7c86212 All but the two biggest .h files have docs. 2024-03-06 12:31:17 -05:00
794804e27f A few more .h file docs. 2024-03-05 21:17:20 -05:00
6d89c1da6e Format. 2024-03-05 20:49:30 -05:00
d059554464 Some workarounds for Haiku. uv_fs_scandir can't tell if a dirent is a file. setrlimit doesn't do anything productive for us. 2024-03-05 20:49:16 -05:00
3a392d4a9f More .h docs. 2024-03-05 12:47:58 -05:00
e3071b372a Poking at TCP binds from Haiku. 2024-03-04 21:51:27 -05:00
18bd279b0c Some progress on .h docs, and add a preliminary CONTRIBUTING.md. 2024-03-04 12:23:00 -05:00
5b93db7463 A buncha muncha cruncha .h docs. Also add vim temporary files to .gitignore. 2024-03-03 18:12:44 -05:00
5b7e5eb91b Give fts a better chance of working with jsonb messages.content. 2024-03-03 18:55:58 +00:00
78ca383e3c http.h docs. 2024-03-03 12:35:10 -05:00
c1eed9ada3 Fixed a leak in ssb.getServerIdentity(). 2024-03-03 12:20:03 -05:00
8d6feb5394 Set the root of private messages correct so that other clients show them. 2024-03-03 12:09:03 -05:00
42994f8977 Make the SSB network key configurable by command-line argument. 2024-03-02 15:01:09 -05:00
f0a871e1f8 More docs. 2024-03-01 21:18:12 -05:00
a710c30572 Fix apps for jsonb. 2024-02-29 19:26:56 -05:00
c991763b00 tests.h and tlscontext.js.h docs. 2024-02-28 21:18:59 -05:00
72dae14f87 Android NDK update. 2024-02-28 21:04:22 -05:00
5800340762 Fix up some more jsonb references. 2024-02-28 20:41:27 -05:00
c5f5adcac6 Missed some more jsonb messages.content use issues. 2024-02-28 20:31:25 -05:00
591642efb3 Convert messages.content to JSONB. This is a very disruptive change. 2024-02-28 20:01:52 -05:00
6182ffa1d4 Docs for tls.h and trace.h. 2024-02-28 19:12:41 -05:00
402a898d96 Let's start working on 0.0.17. 2024-02-28 18:47:21 -05:00
13d43d8319 Let's release 0.0.16. 2024-02-28 18:24:12 -05:00
7bcdbd3813 Revert "Update commonmark.js to 0.31.0."
This reverts commit 165f25db69.
2024-02-26 12:34:50 -05:00
60ada22674 Add a button to toggle visible whitespace for now. Not yet persisted. 2024-02-25 22:31:31 -05:00
637119d46d ideviceinstaller makes this unnecessary. 2024-02-25 21:41:32 -05:00
40f3da6a65 Fix a leak in returning HTTP responses. 2024-02-25 19:38:00 -05:00
f4697fe7f7 Update CodeMirror. 2024-02-25 18:54:35 -05:00
3bc18b9021 Docs for util.js.h. 2024-02-25 18:52:34 -05:00
c21581aefa Use zipalign w/zopfli for APKs to save a little on size. 2024-02-25 18:29:10 -05:00
165f25db69 Update commonmark.js to 0.31.0. 2024-02-25 16:25:23 -05:00
9aa0617aa1 Fix android argv. 2024-02-25 16:02:56 -05:00
ddce88dce6 Merge branch 'tasiaiso-wiki-improvements' 2024-02-25 15:37:56 -05:00
6aa2bce2be Merge branch 'wiki-improvements' of https://dev.tildefriends.net/tasiaiso/tildefriends into tasiaiso-wiki-improvements 2024-02-25 15:37:13 -05:00
a43c1d3d1e Format. 2024-02-25 15:03:43 -05:00
1ed0e817e8 BSD compile fix. 2024-02-25 14:57:14 -05:00
709ca55e65 Fix overbuild on macos. 2024-02-25 14:52:35 -05:00
8c13f5dbba xopt => getopt_long. I give up on xopt. It didn't help me as much as I had hoped, and I had problems building for mingw with only some versions of GCC. Not worth any further time. 2024-02-25 14:45:31 -05:00
4cb82d81b7 apps/gg doesn't belong here and isn't ready for prime time.. 2024-02-24 11:19:36 -05:00
0c42921387 Appease prettier in index.html. 2024-02-24 11:16:07 -05:00
70a3e7fc7d Make app export append a trailing newline to the app.json files so that we match prettier. 2024-02-24 11:12:35 -05:00
d5267be38c Run prettier. 2024-02-24 11:09:34 -05:00
8e7e0ed490 Merge branch 'tasiaiso-prettier' 2024-02-24 11:03:36 -05:00
8cf2837725 Export app json files indented with tabs. 2024-02-24 10:58:53 -05:00
63ae186c76 Export app json files indented with tabs. 2024-02-24 10:55:09 -05:00
dbf5c7b832 Merge branch 'main' into prettier 2024-02-23 09:50:49 +00:00
bfbfc01e99 keep the new config files 2024-02-23 10:42:26 +01:00
8fa9d0e843 Revert "build: Add prettier to the project"
This reverts commit 41024ddb79.
2024-02-23 10:35:39 +01:00
2d3e108fd9 Reapply "build: Add prettier to the project"
This reverts commit 7822b30dcb.
2024-02-23 10:29:46 +01:00
7822b30dcb Revert "build: Add prettier to the project"
This reverts commit 41024ddb79.
2024-02-23 10:25:51 +01:00
2701b7d04e Address some gcc-13 analyzer warnings. #33 2024-02-22 20:13:51 -05:00
e361c3f975 chore(wiki): the button class is now optional for input elements 2024-02-22 22:34:11 +01:00
260706c172 chore: copy .prettierrc.yaml over to client.js 2024-02-22 21:31:15 +01:00
390668ec34 Merge branch 'master' into prettier 2024-02-22 21:23:39 +01:00
1d5cdf9607 feat(wiki): improvements to the wiki's UI 2024-02-22 18:39:53 +01:00
a4bf3542e0 Merge branch 'main' into wiki-improvements 2024-02-22 15:14:49 +00:00
df82cfe66b chore rename core.css to tildefriends.css, remove license from tildefriends.css 2024-02-22 16:11:49 +01:00
f23414adaf prevent previous commits from appearing in git blame 2024-02-22 15:37:40 +01:00
41024ddb79 build: Add prettier to the project 2024-02-22 15:36:45 +01:00
53f9547cc5 style(wiki): use core.js 2024-02-22 13:03:21 +01:00
4bfd9de100 Give iOS the same openssl build treatment as android and mingw. #11 2024-02-21 20:23:35 -05:00
c01e00d77d Get geckodriver.log from 'tildefriends test -t auto' out of the root. 2024-02-21 20:06:25 -05:00
825191c08f Fix 'make dist'. 2024-02-21 19:59:26 -05:00
9dc6670795 Fetch and build OpenSSL as part of the code build. 2024-02-21 19:46:28 -05:00
1db8eee9f7 Merge pull request 'Documentation, build improvements' (#1) from tasiaiso/tildefriends:dev_tasia into main
Reviewed-on: cory/tildefriends#1
2024-02-21 23:51:01 +00:00
1bc50cb62c Remove prebuilt OpenSSL from source control. #11 2024-02-21 12:31:05 -05:00
450b07fd08 Add a Doxyfile and preliminary module-level docs. 2024-02-20 21:41:37 -05:00
2628 changed files with 12336 additions and 560599 deletions

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Add prettier to the project
41024ddb7961b04a5688bbc997cb74de6fab4763

12
.gitignore vendored
View File

@ -1,4 +1,10 @@
.keys
out
**/node_modules
db.* db.*
deps/ios_toolchain/
deps/openssl/
dist/
.keys
**/node_modules
out
*.swo
*.swp
.zsign_cache/

14
.prettierignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
src
deps
.clang-format
# Minified files
**/*.min.css
**/*.min.js
**/leaflet.*
**/commonmark*
**/w3.css
apps/ssb/tribute.esm.js
apps/api/app.js
**/emojis.json

5
.prettierrc.yaml Normal file
View File

@ -0,0 +1,5 @@
trailingComma: 'es5'
useTabs: true
semi: true
singleQuote: true
bracketSpacing: false

37
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,37 @@
# Contributing to Tilde Friends
Thank you for your interest in Tilde Friends.
Above all, Tilde Friends aims to be a fun, safe place to play. When that is at
odds with the course of development, we will work through it with respectful
communication.
## How can I contribute?
The nature of Tilde Friends makes for a wide range of ways to contribute
- Just use it. Really, just kicking the tires will probably shake out issues
in useful ways at this point.
- Report and comment on bugs: https://dev.tildefriends.net/issues.
- Make apps. You don't need my permission to make and share apps with Tilde
Friends. I hope that an ecosystem of good apps grows outside of this
repository. If you want to recreate better versions of the stock apps, just
do it. If you make a better ssb app or whatever and drop me a line however
is most convenient for you, I will probably take a look and consider
replacing the stock one with it.
- Write about it. Docs in the git repository, blog posts, private messages to
me with ideas...really there is no wrong answer. Just make some noise, and
I'll do my best to incorporate or otherwise link your feedback and make the
most of it.
- Write C code in the git repository. I'm really striving for it to be the
case that other people don't really need to meddle in there, but if you can
help out, I will gladly review your pull requests via
https://dev.tildefriends.net/pulls.
## Best practices
- The C code is formatted with clang-format. Run `make format`.
- The rest is formatted with prettier. Run `npm run prettier`.
- We strive to have code compile on all platforms with no warnings and run with
no sanitizer issues.
- There are tests. Run `out/debug/tildefriends test`.

2614
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,12 @@
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 16 VERSION_CODE := 17
VERSION_NUMBER := 0.0.16-wip VERSION_NUMBER := 0.0.17
VERSION_NAME := Medium English breakfast tea. VERSION_NAME := Please enjoy responsibly.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450200.zip
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
PROJECT = tildefriends PROJECT = tildefriends
BUILD_DIR ?= out BUILD_DIR ?= out
@ -55,7 +58,7 @@ CFLAGS += \
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0 ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34 ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.1.10909125 ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342
ANDROID_MIN_SDK_VERSION := 24 ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34 ANDROID_TARGET_SDK_VERSION := 34
@ -209,14 +212,14 @@ $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/li
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif endif
endif endif
ifeq ($(UNAME_M),aarch64) ifeq ($(UNAME_M),aarch64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif endif
get_objs = \ get_objs = \
@ -245,7 +248,6 @@ $(APP_OBJS): CFLAGS += \
-Ideps/quickjs \ -Ideps/quickjs \
-Ideps/sqlite \ -Ideps/sqlite \
-Ideps/valgrind \ -Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \ -Wdouble-promotion \
-Werror -Werror
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
@ -497,18 +499,6 @@ $(SQLITE_OBJS): CFLAGS += \
-Wno-unused-function \ -Wno-unused-function \
-Wno-unused-variable -Wno-unused-variable
XOPT_SOURCES := deps/xopt/xopt.c
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
-DHAVE_SNPRINTF \
-DHAVE_VSNPRINTF \
-DHAVE_VASNPRINTF \
-DHAVE_VASPRINTF \
-Dvsnprintf=rpl_vsnprintf
$(XOPT_OBJS): CFLAGS += \
-Wno-implicit-const-int-float-conversion \
-Wno-pointer-to-int-cast
QUICKJS_SOURCES := \ QUICKJS_SOURCES := \
deps/quickjs/cutils.c \ deps/quickjs/cutils.c \
deps/quickjs/libbf.c \ deps/quickjs/libbf.c \
@ -588,7 +578,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \ LDFLAGS += \
-pthread \ -pthread \
-lm -lm
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ $(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-lssl \ -lssl \
-lcrypto -lcrypto
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
@ -637,8 +627,7 @@ ALL_APP_OBJS := \
$(QUICKJS_OBJS) \ $(QUICKJS_OBJS) \
$(SODIUM_OBJS) \ $(SODIUM_OBJS) \
$(SQLITE_OBJS) \ $(SQLITE_OBJS) \
$(UV_OBJS) \ $(UV_OBJS)
$(XOPT_OBJS)
DEPS = $(ALL_APP_OBJS:.o=.d) DEPS = $(ALL_APP_OBJS:.o=.d)
-include $(DEPS) -include $(DEPS)
@ -717,7 +706,7 @@ PACKAGE_DIRS := \
deps/prettier/ \ deps/prettier/ \
deps/lit/ deps/lit/
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f)) RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
@ -736,10 +725,11 @@ out/apk/TildeFriends-arm-%.unsigned.apk:
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so @cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@cp out/apk/res.apk $@ @cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/ @cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../ @cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@zip -u $@ -q -9 $(RAW_FILES) @zip -u $@.zip -q -9 $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/apk/TildeFriends-x86-%.unsigned.apk: out/apk/TildeFriends-x86-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/ @mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
@ -748,16 +738,22 @@ out/apk/TildeFriends-x86-%.unsigned.apk:
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so @cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so @$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@cp out/apk/res.apk $@ @cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/ @cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../ @cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@zip -u $@ -q -9 $(RAW_FILES) @zip -u $@.zip -q -9 $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/%.apk: out/apk/%.unsigned.apk out/%.apk: out/apk/%.unsigned.apk
@echo "[apksigner] $(notdir $@)" @echo "[apksigner] $(notdir $@)"
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $< @$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk out/%.zopfli.apk: out/%.apk
@echo "[zopfli] $(notdir $@)"
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
.PHONY: release-apk .PHONY: release-apk
releaseapkgo: out/TildeFriends-arm-release.apk releaseapkgo: out/TildeFriends-arm-release.apk
@ -773,10 +769,10 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@cp -v $< $@ @cp -v $< $@
out/%/data.zip: $(RAW_FILES) out/data.zip: $(RAW_FILES)
@zip -u $@ -q -9 $(RAW_FILES) @zip -u $@ -q -9 $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@cp -v $< $@ @cp -v $< $@
ifeq ($(HAVE_LINUX_IOS),1) ifeq ($(HAVE_LINUX_IOS),1)
@ -791,6 +787,12 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./ @cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
@rm -rf $@.tmp/ @rm -rf $@.tmp/
out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
@echo "[standalone] $@"
@cat $< out/data.zip > $@
@chmod +x $@
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
@ -814,30 +816,49 @@ apklog:
fetchdeps: fetchdeps:
@echo "[fetch] libuv" @echo "[fetch] libuv"
@test -f out/deps/libuv.tar.gz || (mkdir -p out/deps/ && curl -q https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz -o out/deps/libuv.tar.gz) @test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz)
@test -d deps/libuv/ || (mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz) @test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
@echo -n $(LIBUV_URL) > out/deps/libuv.txt
@echo "[fetch] sqlite" @echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip || (mkdir -p out/deps/ && curl -q https://www.sqlite.org/2024/sqlite-amalgamation-3450100.zip -o out/deps/sqlite.zip) @test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ || (mkdir -p deps/sqlite/ && unzip -qDj -d deps/sqlite/ out/deps/sqlite.zip) @test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier" @echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs @test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs @test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs @test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs @test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONE: fetchdeps .PHONY: fetchdeps
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@tools/ssl-android
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@tools/ssl-mingw64
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
endif
ifeq ($(UNAME_S),Darwin)
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@tools/ssl-ios
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
clean: clean:
rm -rf $(BUILD_DIR) rm -rf $(BUILD_DIR)
.PHONY: clean .PHONY: clean
dist: release-apk iosrelease-ipa dist: release-apk iosrelease-ipa
@echo "[export] $$(svn info --show-item url)" @echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf tildefriends-$(VERSION_NUMBER) @rm -rf out/tildefriends-$(VERSION_NUMBER)
@svn export -q . tildefriends-$(VERSION_NUMBER) @mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION @git archive HEAD | tar -x -C out/tildefriends-$(VERSION_NUMBER)
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
@tar \ @tar \
--exclude=apps/gg* \
--exclude=apps/welcome* \ --exclude=apps/welcome* \
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \ --exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
--exclude=deps/libsodium/builds/msvc/vs* \ --exclude=deps/libsodium/builds/msvc/vs* \
@ -852,14 +873,15 @@ dist: release-apk iosrelease-ipa
--exclude=deps/sqlite/shell.c \ --exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \ --exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \ --exclude=deps/zlib/doc \
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER) -caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
@rm -rf tildefriends-$(VERSION_NUMBER) -C out/ \
tildefriends-$(VERSION_NUMBER)
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk" @echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-x86-release.apk TildeFriends-x86-$(VERSION_NUMBER).apk @cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk" @echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-arm-release.apk TildeFriends-arm-$(VERSION_NUMBER).apk @cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa" @echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
@cp out/tildefriends-release.ipa TildeFriends-$(VERSION_NUMBER).ipa @cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
.PHONY: dist .PHONY: dist
dist-test: dist dist-test: dist
@ -872,3 +894,11 @@ dist-test: dist
format: format:
@clang-format -i $(wildcard src/*.c src/*.h src/*.m) @clang-format -i $(wildcard src/*.c src/*.h src/*.m)
.PHONY: format .PHONY: format
prettier:
@npm run prettier
.PHONY: prettier
docs:
@doxygen
.PHONY: docs

View File

@ -1,4 +1,5 @@
# Tilde Friends # Tilde Friends
Tilde Friends is a tool for making and sharing. Tilde Friends is a tool for making and sharing.
A public instance lives at https://www.tildefriends.net/. A public instance lives at https://www.tildefriends.net/.
@ -7,12 +8,14 @@ It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, as well as a platform for writing and running web applications. Scuttlebutt, as well as a platform for writing and running web applications.
## Goals ## Goals
1. Make it easy and fun to run all sorts of web applications. 1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data. 2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a 3. Make creating and sharing web applications accessible to anyone with a
browser. browser.
## Building ## Building
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android. all of those host platforms plus mingw64, iOS, and android.
@ -27,6 +30,7 @@ all of those host platforms plus mingw64, iOS, and android.
5. `make format` will normalize formatting to the coding standard. 5. `make format` will normalize formatting to the coding standard.
## Running ## Running
By default, running the built `tildefriends` executable will start a web server By default, running the built `tildefriends` executable will start a web server
at <http://localhost:12345/>. `tildefriends -h` lists further options. at <http://localhost:12345/>. `tildefriends -h` lists further options.
@ -35,9 +39,11 @@ privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>. <http://localhost:12345/~core/admin/>.
## Documentation ## Documentation
Docs are a work in progress: Docs are a work in progress:
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>. <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
## License ## License
All code unless otherwise noted in is provided under the All code unless otherwise noted in is provided under the
[MIT](https://opensource.org/licenses/MIT) license. [MIT](https://opensource.org/licenses/MIT) license.

View File

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

View File

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

View File

@ -3,25 +3,32 @@ import * as tfrpc from '/static/tfrpc.js';
function delete_user(user) { function delete_user(user) {
if (confirm(`Are you sure you want to delete the user "${user}"?`)) { if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
tfrpc.rpc.delete_user(user).then(function() { tfrpc.rpc
.delete_user(user)
.then(function () {
alert(`User "${user}" deleted successfully.`); alert(`User "${user}" deleted successfully.`);
}).catch(function(error) { })
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`); .catch(function (error) {
alert(
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
);
}); });
} }
} }
function global_settings_set(key, value) { function global_settings_set(key, value) {
tfrpc.rpc.global_settings_set(key, value).then(function() { tfrpc.rpc
.global_settings_set(key, value)
.then(function () {
alert(`Set "${key}" to "${value}".`); alert(`Set "${key}" to "${value}".`);
}).catch(function(error) { })
.catch(function (error) {
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`); alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
}); });
} }
window.addEventListener('load', function () { window.addEventListener('load', function () {
const permission_template = (permission) => const permission_template = (permission) => html` <code>${permission}</code>`;
html` <code>${permission}</code>`;
function input_template(key, description) { function input_template(key, description) {
if (description.type === 'boolean') { if (description.type === 'boolean') {
return html` return html`
@ -62,26 +69,24 @@ window.addEventListener('load', function() {
} }
const user_template = (user, permissions) => html` const user_template = (user, permissions) => html`
<li> <li>
<button @click=${(e) => delete_user(user)}> <button @click=${(e) => delete_user(user)}>Delete</button>
Delete ${user}: ${permissions.map((x) => permission_template(x))}
</button>
${user}:
${permissions.map(x => permission_template(x))}
</li> </li>
`; `;
const users_template = (users) => const users_template = (users) =>
html`<h2>Users</h2> html`<h2>Users</h2>
<ul> <ul>
${Object.entries(users).map(u => user_template(u[0], u[1]))} ${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`; </ul>`;
const page_template = (data) => const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%"> html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<h2>Global Settings</h2> <h2>Global Settings</h2>
<div> <div>
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)} ${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
</div> </div>
${users_template(data.users)} ${users_template(data.users)}
</div> </div> `;
`;
render(page_template(g_data), document.body); render(page_template(g_data), document.body);
}); });

View File

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

View File

@ -26,11 +26,12 @@ async function fetch_info(apps) {
async function fetch_shared_apps() { async function fetch_shared_apps() {
let messages = {}; let messages = {};
await ssb.sqlAsync(` await ssb.sqlAsync(
SELECT messages.* `
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts('"application/tildefriends"') FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp ORDER BY messages.timestamp
`, `,
[], [],
function (row) { function (row) {
@ -44,10 +45,13 @@ async function fetch_shared_apps() {
}; };
} }
} }
}); }
);
let result = {}; let result = {};
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) { for (let app of Object.values(messages).sort(
(x, y) => y.message.timestamp - x.message.timestamp
)) {
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob))); let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
if (app_object) { if (app_object) {
app_object.blob_id = app.blob; app_object.blob_id = app.blob;

View File

@ -1,11 +1,19 @@
import * as commonmark from './commonmark.min.js'; import * as commonmark from './commonmark.min.js';
function escape(text) { function escape(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;'); return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
} }
function escapeAttribute(text) { function escapeAttribute(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;'); return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
} }
export async function get_blog_message(id) { export async function get_blog_message(id) {
@ -21,7 +29,8 @@ export async function get_blog_message(id) {
blog: content?.blog, blog: content?.blog,
title: content?.title, title: content?.title,
}; };
}); }
);
if (message) { if (message) {
await ssb.sqlAsync( await ssb.sqlAsync(
` `
@ -36,7 +45,8 @@ export async function get_blog_message(id) {
[message.author], [message.author],
function (row) { function (row) {
message.name = row.name; message.name = row.name;
}); }
);
} }
return message; return message;
} }
@ -51,8 +61,12 @@ export function markdown(md) {
node = event.node; node = event.node;
if (event.entering) { if (event.entering) {
if (node.destination?.startsWith('&')) { if (node.destination?.startsWith('&')) {
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal; node.destination =
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) { '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (
node.destination?.startsWith('@') ||
node.destination?.startsWith('%')
) {
node.destination = '/~core/ssb/#' + escape(node.destination); node.destination = '/~core/ssb/#' + escape(node.destination);
} }
} }
@ -107,7 +121,7 @@ export function render_html(blogs) {
<h1>🪵Tilde Friends Blog</h1> <h1>🪵Tilde Friends Blog</h1>
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div> <div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
</div> </div>
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')} ${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
</body> </body>
</html>`; </html>`;
} }
@ -135,14 +149,15 @@ export function render_atom(blogs) {
<link href="${core.url}"/> <link href="${core.url}"/>
<id>${core.url}</id> <id>${core.url}</id>
<updated>${new Date().toString()}</updated> <updated>${new Date().toString()}</updated>
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')} ${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
</feed>`; </feed>`;
} }
export async function get_posts() { export async function get_posts() {
let blogs = []; let blogs = [];
let ids = await ssb.getIdentities(); let ids = await ssb.getIdentities();
await ssb.sqlAsync(` await ssb.sqlAsync(
`
WITH WITH
blogs AS ( blogs AS (
SELECT SELECT
@ -182,8 +197,11 @@ export async function get_posts() {
JOIN public ON public.author = blogs.author JOIN public ON public.author = blogs.author
LEFT OUTER JOIN names ON names.author = blogs.author LEFT OUTER JOIN names ON names.author = blogs.author
ORDER BY blogs.timestamp DESC LIMIT 20 ORDER BY blogs.timestamp DESC LIMIT 20
`, [JSON.stringify(ids)], function(row) { `,
[JSON.stringify(ids)],
function (row) {
blogs.push(row); blogs.push(row);
}); }
);
return blogs; return blogs;
} }

View File

@ -2,30 +2,50 @@ import * as blog from './blog.js';
async function main() { async function main() {
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) { if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path; let id = request.path.startsWith('%25')
? '%' + request.path.substring(3)
: request.path;
let message = await blog.get_blog_message(id); let message = await blog.get_blog_message(id);
if (message) { if (message) {
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'}); respond({
data: await blog.render_blog_post_html(message),
content_type: 'text/html; charset=utf-8',
});
} else { } else {
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'}); respond({
data: `Message ${id} not found.`,
content_type: 'text/html; charset=utf-8',
});
} }
} else if (request.path == 'atom') { } else if (request.path == 'atom') {
let blogs = await blog.get_posts(); let blogs = await blog.get_posts();
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'}); respond({
data: blog.render_atom(blogs),
content_type: 'application/atom+xml',
});
} else { } else {
let blogs = await blog.get_posts(); let blogs = await blog.get_posts();
for (let blog_post of blogs) { for (let blog_post of blogs) {
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase(); let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
if (request.path === title) { if (request.path === title) {
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'}); respond({
data: await blog.render_blog_post_html(blog_post),
content_type: 'text/html; charset=utf-8',
});
return; return;
} }
} }
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'}); respond({
data: blog.render_html(blogs),
content_type: 'text/html; charset=utf-8',
});
} }
} }
main().catch(function (error) { main().catch(function (error) {
respond({data: `<!DOCTYPE html> respond({
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'}); data: `<!DOCTYPE html>
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
content_type: 'text/html',
});
}); });

View File

@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
json_extract(content, '$.type') = 'contact' json_extract(content, '$.type') = 'contact'
ORDER BY sequence ORDER BY sequence
`, `,
[id, last_row_id, max_row_id]); [id, last_row_id, max_row_id]
);
for (let row of contacts) { for (let row of contacts) {
let contact = JSON.parse(row.content); let contact = JSON.parse(row.content);
if (contact.following === true) { if (contact.following === true) {
@ -42,15 +43,34 @@ async function contact(id, last_row_id, following, max_row_id) {
return await contacts_internal(id, last_row_id, following, max_row_id); return await contacts_internal(id, last_row_id, following, max_row_id);
} }
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) { async function following_deep_internal(
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id))); ids,
depth,
blocking,
last_row_id,
following,
max_row_id
) {
let contacts = await Promise.all(
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
);
let result = {}; let result = {};
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
let id = ids[i]; let id = ids[i];
let contact = contacts[i]; let contact = contacts[i];
let all_blocking = Object.assign({}, contact.blocking, blocking); let all_blocking = Object.assign({}, contact.blocking, blocking);
let found = Object.keys(contact.following).filter(y => !all_blocking[y]); let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : []; let deeper =
depth > 1
? await following_deep_internal(
found,
depth - 1,
all_blocking,
last_row_id,
following,
max_row_id
)
: [];
result[id] = [id, ...found, ...deeper]; result[id] = [id, ...found, ...deeper];
} }
return [...new Set(Object.values(result).flat())]; return [...new Set(Object.values(result).flat())];
@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) {
last_row_id: 0, last_row_id: 0,
}; };
} }
let max_row_id = (await query(` let max_row_id = (
await query(
`
SELECT MAX(rowid) AS max_row_id FROM messages SELECT MAX(rowid) AS max_row_id FROM messages
`, []))[0].max_row_id; `,
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id); []
)
)[0].max_row_id;
let result = await following_deep_internal(
ids,
depth,
blocking,
cache.last_row_id,
cache.following,
max_row_id
);
cache.last_row_id = max_row_id; cache.last_row_id = max_row_id;
let store = JSON.stringify(cache); let store = JSON.stringify(cache);
await db.set('following', store); await db.set('following', store);
@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) {
}; };
} }
let max_row_id = 0; let max_row_id = 0;
await ssb.sqlAsync(` await ssb.sqlAsync(
`
SELECT MAX(rowid) AS max_row_id FROM messages SELECT MAX(rowid) AS max_row_id FROM messages
`, `,
[], [],
function (row) { function (row) {
max_row_id = row.max_row_id; max_row_id = row.max_row_id;
}); }
);
for (let id of Object.keys(cache.about)) { for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) { if (ids.indexOf(id) == -1) {
delete cache.about[id]; delete cache.about[id];
@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) {
ORDER BY messages.author, messages.sequence ORDER BY messages.author, messages.sequence
`, `,
[ [
JSON.stringify(ids.filter(id => cache.about[id])), JSON.stringify(ids.filter((id) => cache.about[id])),
JSON.stringify(ids.filter(id => !cache.about[id])), JSON.stringify(ids.filter((id) => !cache.about[id])),
cache.last_row_id, cache.last_row_id,
max_row_id, max_row_id,
]); ]
);
for (let about of abouts) { for (let about of abouts) {
let content = JSON.parse(about.content); let content = JSON.parse(about.content);
if (content.about === about.author) { if (content.about === about.author) {
delete content.type; delete content.type;
delete content.about; delete content.about;
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content); cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
} }
} }
cache.last_row_id = max_row_id; cache.last_row_id = max_row_id;
@ -155,24 +193,24 @@ async function getAbout(db, id) {
if (g_about_cache[id]) { if (g_about_cache[id]) {
return g_about_cache[id]; return g_about_cache[id];
} }
let o = await db.get(id + ":about"); let o = await db.get(id + ':about');
const k_version = 4; const k_version = 4;
let f = o ? JSON.parse(o) : o; let f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) { if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version}; f = {about: {}, sequence: 0, version: k_version};
} }
await ssb.sqlAsync( await ssb.sqlAsync(
"SELECT "+ 'SELECT ' +
" sequence, "+ ' sequence, ' +
" content "+ ' content ' +
"FROM messages "+ 'FROM messages ' +
"WHERE "+ 'WHERE ' +
" author = ?1 AND "+ ' author = ?1 AND ' +
" sequence > ?2 AND "+ ' sequence > ?2 AND ' +
" json_extract(content, '$.type') = 'about' AND " + " json_extract(content, '$.type') = 'about' AND " +
" json_extract(content, '$.about') = ?1 " + " json_extract(content, '$.about') = ?1 " +
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+ 'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
"ORDER BY sequence", 'ORDER BY sequence',
[id, f.sequence], [id, f.sequence],
function (row) { function (row) {
f.sequence = row.sequence; f.sequence = row.sequence;
@ -180,16 +218,16 @@ async function getAbout(db, id) {
let about = {}; let about = {};
try { try {
about = JSON.parse(row.content); about = JSON.parse(row.content);
} catch { } catch {}
}
delete about.about; delete about.about;
delete about.type; delete about.type;
f.about = Object.assign(f.about, about); f.about = Object.assign(f.about, about);
} }
}); }
);
let j = JSON.stringify(f); let j = JSON.stringify(f);
if (o != j) { if (o != j) {
await db.set(id + ":about", j); await db.set(id + ':about', j);
} }
g_about_cache[id] = f.about; g_about_cache[id] = f.about;
return f.about; return f.about;
@ -198,15 +236,15 @@ async function getAbout(db, id) {
async function getSize(db, id) { async function getSize(db, id) {
let size = 0; let size = 0;
await ssb.sqlAsync( await ssb.sqlAsync(
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1", 'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
[id], [id],
function (row) { function (row) {
size += row.size; size += row.size;
}); }
);
return size; return size;
} }
async function getSizes(ids) { async function getSizes(ids) {
let sizes = {}; let sizes = {};
await ssb.sqlAsync( await ssb.sqlAsync(
@ -221,7 +259,8 @@ async function getSizes(ids) {
[JSON.stringify(ids)], [JSON.stringify(ids)],
function (row) { function (row) {
sizes[row.author] = row.size; sizes[row.author] = row.size;
}); }
);
return sizes; return sizes;
} }
@ -241,7 +280,10 @@ function niceSize(bytes) {
} }
function escape(value) { function escape(value) {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;'); return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
} }
async function main() { async function main() {
@ -249,19 +291,27 @@ async function main() {
let db = await database('ssb'); let db = await database('ssb');
let whoami = await ssb.getIdentities(); let whoami = await ssb.getIdentities();
let tree = ''; let tree = '';
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`); await app.setDocument(
`<pre style="color: #fff">Enumerating followed users...</pre>`
);
let following = await following_deep(whoami, 2, {}); let following = await following_deep(whoami, 2, {});
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`); await app.setDocument(
`<pre style="color: #fff">Getting names and sizes...</pre>`
);
let [about, sizes] = await Promise.all([ let [about, sizes] = await Promise.all([
fetch_about(db, following, {}), fetch_about(db, following, {}),
getSizes(following), getSizes(following),
]); ]);
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`); await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0))); following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
for (let id of following) { for (let id of following) {
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`; tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
} }
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>'); await app.setDocument(
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
tree +
'</ul>\n</body>\n</html>'
);
} }
main(); main();

View File

@ -1,5 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "🗺",
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

120
apps/gg/lit-all.min.js vendored

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ tfrpc.register(async function reload() {
async function main() { async function main() {
let ids = await ssb.getIdentities(); let ids = await ssb.getIdentities();
await app.setDocument(`<body style="color: #fff"> await app.setDocument(
`<body style="color: #fff">
<script>const handler = {};</script> <script>const handler = {};</script>
<script type="module"> <script type="module">
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
@ -75,13 +76,18 @@ async function main() {
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button> <textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
<h2>Identities</h2> <h2>Identities</h2>
<ul>` + <ul>` +
ids.map(id => `<li> ids
.map(
(id) => `<li>
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button> <button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button> <button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
${id} ${id}
</li>`).join('\n')+ </li>`
)
.join('\n') +
` </ul> ` </ul>
</body>`); </body>`
);
} }
main(); main();

View File

@ -97,7 +97,7 @@ core.register('onConnectionsChanged', async function() {
}); });
async function main() { async function main() {
if (typeof(database) !== 'undefined') { if (typeof database !== 'undefined') {
g_database = await database('ssb'); g_database = await database('ssb');
} }
await app.setDocument(utf8Decode(await getFile('index.html'))); await app.setDocument(utf8Decode(await getFile('index.html')));

View File

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

View File

@ -31,7 +31,12 @@ class TfIdPickerElement extends LitElement {
if (this.ids) { if (this.ids) {
return html` return html`
<select @change=${this.changed} style="max-width: 100%"> <select @change=${this.changed} style="max-width: 100%">
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)} ${this.ids.map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${id}
</option>`
)}
</select> </select>
`; `;
} else { } else {
@ -57,13 +62,15 @@ class TfComposeElement extends LitElement {
} }
submit() { submit() {
this.dispatchEvent(new CustomEvent('tf-submit', { this.dispatchEvent(
new CustomEvent('tf-submit', {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { detail: {
value: this.renderRoot.getElementById('input').value, value: this.renderRoot.getElementById('input').value,
}, },
})); })
);
this.renderRoot.getElementById('input').value = ''; this.renderRoot.getElementById('input').value = '';
this.input(); this.input();
} }
@ -96,7 +103,8 @@ class TfIssuesAppElement extends LitElement {
async load() { async load() {
let issues = {}; let issues = {};
let messages = await tfrpc.rpc.query(` let messages = await tfrpc.rpc.query(
`
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
messages.id = messages_refs.message messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
@ -107,7 +115,9 @@ class TfIssuesAppElement extends LitElement {
SELECT * FROM issues SELECT * FROM issues
UNION UNION
SELECT * FROM edits ORDER BY timestamp SELECT * FROM edits ORDER BY timestamp
`, [k_project]); `,
[k_project]
);
for (let message of messages) { for (let message of messages) {
let content = JSON.parse(message.content); let content = JSON.parse(message.content);
switch (content.type) { switch (content.type) {
@ -123,7 +133,7 @@ class TfIssuesAppElement extends LitElement {
break; break;
case 'issue-edit': case 'issue-edit':
case 'post': case 'post':
for (let issue of (content.issues || [])) { for (let issue of content.issues || []) {
if (issues[issue.link]) { if (issues[issue.link]) {
if (issue.open !== undefined) { if (issue.open !== undefined) {
issues[issue.link].open = issue.open; issues[issue.link].open = issue.open;
@ -136,7 +146,9 @@ class TfIssuesAppElement extends LitElement {
break; break;
} }
} }
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created)); this.issues = Object.values(issues).sort(
(x, y) => y.open - x.open || y.created - x.created
);
if (this.selected) { if (this.selected) {
for (let issue of this.issues) { for (let issue of this.issues) {
if (issue.id == this.selected.id) { if (issue.id == this.selected.id) {
@ -150,11 +162,20 @@ class TfIssuesAppElement extends LitElement {
return html` return html`
<tr> <tr>
<td>${issue.open ? '☐ open' : '☑ closed'}</td> <td>${issue.open ? '☐ open' : '☑ closed'}</td>
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td> <td
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}> style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
${issue.author}
</td>
<td
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
@click=${() => (this.selected = issue)}
>
${issue.text.split('\n')?.[0]} ${issue.text.split('\n')?.[0]}
</td> </td>
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td> <td>
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
</td>
</tr> </tr>
`; `;
} }
@ -170,13 +191,21 @@ class TfIssuesAppElement extends LitElement {
<div>${new Date(update.timestamp).toLocaleString()}</div> <div>${new Date(update.timestamp).toLocaleString()}</div>
<div>${update.author}</div> <div>${update.author}</div>
<div>${message}</div> <div>${message}</div>
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div> <div>
${update.open !== undefined
? update.open
? 'issue opened'
: 'issue closed'
: undefined}
</div>
</div> </div>
`; `;
} }
async set_open(id, open) { async set_open(id, open) {
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) { if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
) {
let whoami = this.shadowRoot.getElementById('picker').selected; let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, { await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit', type: 'issue-edit',
@ -207,7 +236,9 @@ class TfIssuesAppElement extends LitElement {
type: 'post', type: 'post',
text: event.detail.value, text: event.detail.value,
root: this.selected.id, root: this.selected.id,
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id, branch: this.selected.updates.length
? this.selected.updates[this.selected.updates.length - 1].id
: this.selected.id,
issues: [ issues: [
{ {
link: this.selected.id, link: this.selected.id,
@ -226,16 +257,18 @@ class TfIssuesAppElement extends LitElement {
return html` return html`
${header} ${header}
<div> <div>
<input type="button" value="Back" @click=${() => this.selected = undefined}></input> <input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
${this.selected.open ? ${
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` : this.selected.open
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`} ? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
}
</div> </div>
<div>${new Date(this.selected.created).toLocaleString()}</div> <div>${new Date(this.selected.created).toLocaleString()}</div>
<div>${this.selected.author}</div> <div>${this.selected.author}</div>
<div>${this.selected.id}</div> <div>${this.selected.id}</div>
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div> <div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
${this.selected.updates.map(x => this.render_update(x))} ${this.selected.updates.map((x) => this.render_update(x))}
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose> <tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
`; `;
} else { } else {
@ -250,7 +283,7 @@ class TfIssuesAppElement extends LitElement {
<th>Title</th> <th>Title</th>
<th>Date</th> <th>Date</th>
</tr> </tr>
${this.issues.map(x => this.render_issue_table_row(x))} ${this.issues.map((x) => this.render_issue_table_row(x))}
</table> </table>
`; `;
} }

View File

@ -1,20 +1,32 @@
import * as linkify from './commonmark-linkify.js'; import * as linkify from './commonmark-linkify.js';
function image(node, entering) { function image(node, entering) {
if (node.firstChild?.type === 'text' && if (
node.firstChild.literal.startsWith('video:')) { node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) { if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>'); 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.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1; this.disableTags += 1;
} else { } else {
this.disableTags -= 1; this.disableTags -= 1;
this.lit('</video>'); this.lit('</video>');
} }
} else if (node.firstChild?.type === 'text' && } else if (
node.firstChild.literal.startsWith('audio:')) { node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) { if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>'); 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.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1; this.disableTags += 1;
} else { } else {
@ -24,7 +36,11 @@ function image(node, entering) {
} else { } else {
if (entering) { if (entering) {
if (this.disableTags === 0) { if (this.disableTags === 0) {
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>'); this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) { if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="'); this.lit('<img src="" alt="');
} else { } else {
@ -56,14 +72,20 @@ export function markdown(md) {
node = event.node; node = event.node;
if (event.entering) { if (event.entering) {
if (node.type == 'link') { if (node.type == 'link') {
if (node.destination.startsWith('@') && if (
node.destination.endsWith('.ed25519')) { node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + node.destination; node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') && } else if (
node.destination.endsWith('.sha256')) { node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + node.destination; node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') && } else if (
node.destination.endsWith('.sha256')) { node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view'; node.destination = '/' + node.destination + '/view';
} }
} else if (node.type == 'image') { } else if (node.type == 'image') {

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "📝", "emoji": "📝",
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256" "previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
} }

View File

@ -104,8 +104,7 @@ async function process_message(whoami, collection, message, kind, parent) {
if (!x) { if (!x) {
return; return;
} }
if (content.type !== kind || if (content.type !== kind || (parent && content.parent !== parent)) {
(parent && content.parent !== parent)) {
return; return;
} }
} }
@ -113,7 +112,10 @@ async function process_message(whoami, collection, message, kind, parent) {
if (content?.tombstone) { if (content?.tombstone) {
delete collection[content.key]; delete collection[content.key];
} else { } else {
collection[content.key] = Object.assign(collection[content.key] || {}, content); collection[content.key] = Object.assign(
collection[content.key] || {},
content
);
} }
} else { } else {
collection[message.id] = Object.assign(content, {id: message.id}); collection[message.id] = Object.assign(content, {id: message.id});
@ -125,20 +127,29 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
let whoami = await ssb.getIdentities(); let whoami = await ssb.getIdentities();
data = data ?? {}; data = data ?? {};
let rowid = 0; let rowid = 0;
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid; rowid = row.rowid;
}); }
);
while (true) { while (true) {
if (rowid == max_rowid) { if (rowid == max_rowid) {
await new_message(); await new_message();
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid; rowid = row.rowid;
}); }
);
} }
let modified = false; let modified = false;
let rows = []; let rows = [];
await ssb.sqlAsync(` await ssb.sqlAsync(
`
SELECT messages.id, author, content, timestamp SELECT messages.id, author, content, timestamp
FROM messages FROM messages
JOIN json_each(?1) AS id ON messages.author = id.value JOIN json_each(?1) AS id ON messages.author = id.value
@ -152,7 +163,8 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent], [JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
function (row) { function (row) {
rows.push(row); rows.push(row);
}); }
);
max_rowid = rowid; max_rowid = rowid;
for (let row of rows) { for (let row of rows) {
if (await process_message(whoami, data, row, kind, parent)) { if (await process_message(whoami, data, row, kind, parent)) {

View File

@ -1,12 +1,14 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<base target="_top"> <base target="_top" />
</head> </head>
<body style="color: #fff"> <body style="color: #fff">
<tf-journal-app></tf-journal-app> <tf-journal-app></tf-journal-app>
<script src="commonmark.min.js"></script> <script src="commonmark.min.js"></script>
<script>window.litDisableBundleWarning = true;</script> <script>
window.litDisableBundleWarning = true;
</script>
<script src="tf-journal-app.js" type="module"></script> <script src="tf-journal-app.js" type="module"></script>
<script src="tf-journal-entry.js" type="module"></script> <script src="tf-journal-entry.js" type="module"></script>
<script src="tf-id-picker.js" type="module"></script> <script src="tf-id-picker.js" type="module"></script>

View File

@ -19,15 +19,22 @@ class TfIdentityPickerElement extends LitElement {
changed(event) { changed(event) {
this.selected = event.srcElement.value; this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', { this.dispatchEvent(
new Event('change', {
srcElement: this, srcElement: this,
})); })
);
} }
render() { render() {
return html` return html`
<select @change=${this.changed} style="max-width: 100%"> <select @change=${this.changed} style="max-width: 100%">
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)} ${(this.ids ?? []).map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${id}
</option>`
)}
</select> </select>
`; `;
} }

View File

@ -28,9 +28,14 @@ class TfJournalAppElement extends LitElement {
async read_journals() { async read_journals() {
let max_rowid; let max_rowid;
let journals; let journals;
while (true) while (true) {
{ [max_rowid, journals] = await tfrpc.rpc.collection(
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals); [this.whoami],
'journal-entry',
undefined,
max_rowid,
journals
);
this.journals = Object.assign({}, journals); this.journals = Object.assign({}, journals);
console.log('JOURNALS', this.journals); console.log('JOURNALS', this.journals);
} }
@ -52,7 +57,11 @@ class TfJournalAppElement extends LitElement {
}; };
message.recps = [this.whoami]; message.recps = [this.whoami];
print(message); print(message);
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message)); message = await tfrpc.rpc.encrypt(
this.whoami,
message.recps,
JSON.stringify(message)
);
print(message); print(message);
await tfrpc.rpc.appendMessage(this.whoami, message); await tfrpc.rpc.appendMessage(this.whoami, message);
} }
@ -62,12 +71,17 @@ class TfJournalAppElement extends LitElement {
let self = this; let self = this;
return html` return html`
<div> <div>
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker> <tf-id-picker
.ids=${this.ids}
selected=${this.whoami}
@change=${this.on_whoami_changed}
></tf-id-picker>
</div> </div>
<tf-journal-entry <tf-journal-entry
whoami=${this.whoami} whoami=${this.whoami}
.journals=${this.journals} .journals=${this.journals}
@publish=${this.on_journal_publish}></tf-journal-entry> @publish=${this.on_journal_publish}
></tf-journal-entry>
`; `;
} }
} }

View File

@ -30,13 +30,15 @@ class TfJournalEntryElement extends LitElement {
async on_publish() { async on_publish() {
console.log('publish', this.text); console.log('publish', this.text);
this.dispatchEvent(new CustomEvent('publish', { this.dispatchEvent(
new CustomEvent('publish', {
bubbles: true, bubbles: true,
detail: { detail: {
key: this.shadowRoot.getElementById('date_picker').value, key: this.shadowRoot.getElementById('date_picker').value,
text: this.text, text: this.text,
}, },
})); })
);
} }
back_dates(count) { back_dates(count) {
@ -63,19 +65,30 @@ class TfJournalEntryElement extends LitElement {
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]); console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
return html` return html`
<select id="date_picker" @change=${this.on_date_change}> <select id="date_picker" @change=${this.on_date_change}>
${this.back_dates(10).map(x => html` ${this.back_dates(10).map(
<option value=${x}>${x}</option> (x) => html` <option value=${x}>${x}</option> `
`)} )}
</select> </select>
<div style="display: inline-flex; flex-direction: row"> <div style="display: inline-flex; flex-direction: row">
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button> <button
?disabled=${this.text == this.journals?.[this.key]?.text}
@click=${this.on_publish}
>
Publish
</button>
<button @click=${this.on_discard}>Discard</button> <button @click=${this.on_discard}>Discard</button>
</div> </div>
<div style="display: flex; flex-direction: row"> <div style="display: flex; flex-direction: row">
<textarea <textarea
style="flex: 1 1; min-height: 10em" style="flex: 1 1; min-height: 10em"
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea> @input=${this.on_edit}
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div> .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
></textarea>
<div style="flex: 1 1">
${unsafeHTML(
this.markdown(this.text ?? this.journals?.[this.key]?.text)
)}
</div>
</div> </div>
`; `;
} }

5
apps/room.json Normal file
View File

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

13
apps/room/app.js Normal file
View File

@ -0,0 +1,13 @@
async function main() {
let host = core.url.match(/.*\/\/(.*?)\//)[1];
let id = (await ssb.getServerIdentity()).substring(1);
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
await app.setDocument(`
<body style="color: #fff">
<h1>Server</h1>
<div>The local server address is:</div>
<div><input type="text" readonly value="${room}" style="width: 100%"></input></div>
</body>
`);
}
main();

View File

@ -1,4 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "👟" "emoji": "👟",
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
} }

View File

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

View File

@ -19,7 +19,8 @@ class TfSneakerAppElement extends LitElement {
async search() { async search() {
let q = this.renderRoot.getElementById('search').value; let q = this.renderRoot.getElementById('search').value;
let result = await tfrpc.rpc.query(` let result = await tfrpc.rpc.query(
`
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
FROM messages_fts(?) FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
@ -31,15 +32,17 @@ class TfSneakerAppElement extends LitElement {
HAVING MAX(messages.sequence) HAVING MAX(messages.sequence)
ORDER BY COUNT(*) DESC ORDER BY COUNT(*) DESC
`, `,
[`"${q.replaceAll('"', '""')}"`]); [`"${q.replaceAll('"', '""')}"`]
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name])); );
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
} }
format_message(message) { format_message(message) {
const k_flag_sequence_before_author = 1;
let out = { let out = {
previous: message.previous ?? null, previous: message.previous ?? null,
}; };
if (message.sequence_before_author) { if (message.flags & k_flag_sequence_before_author) {
out.sequence = message.sequence; out.sequence = message.sequence;
out.author = message.author; out.author = message.author;
} else { } else {
@ -70,24 +73,104 @@ class TfSneakerAppElement extends LitElement {
return true; return true;
} }
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) || if (
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) || 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, 0xee]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) { startsWith(data, [
0xff,
0xd8,
0xff,
0xe1,
null,
null,
0x45,
0x78,
0x69,
0x66,
0x00,
0x00,
])
) {
return '.jpg'; return '.jpg';
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { } else if (
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
) {
return '.png'; return '.png';
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || } else if (
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) { startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
) {
return '.gif'; return '.gif';
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) { } else if (
startsWith(data, [
0x52,
0x49,
0x46,
0x46,
null,
null,
null,
null,
0x57,
0x45,
0x42,
0x50,
])
) {
return '.webp'; return '.webp';
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) { } else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
return '.svg'; return '.svg';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) { } else if (
startsWith(data, [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x6d,
0x70,
0x34,
0x32,
])
) {
return '.mp3'; return '.mp3';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) || } else if (
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) { 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'; return '.mp4';
} else { } else {
return '.bin'; return '.bin';
@ -98,17 +181,34 @@ class TfSneakerAppElement extends LitElement {
let all_messages = ''; let all_messages = '';
let sequence = -1; let sequence = -1;
let messages_done = 0; let messages_done = 0;
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total; let messages_max = (
await tfrpc.rpc.query(
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
[id]
)
)[0].total;
while (true) { while (true) {
let messages = await tfrpc.rpc.query( let messages = await tfrpc.rpc.query(
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100', `
SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
FROM messages
WHERE author = ? AND SEQUENCE > ?
ORDER BY sequence LIMIT 100
`,
[id, sequence] [id, sequence]
); );
if (messages?.length) { if (messages?.length) {
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n'; all_messages +=
messages
.map((x) => JSON.stringify(this.format_message(x)))
.join('\n') + '\n';
sequence = messages[messages.length - 1].sequence; sequence = messages[messages.length - 1].sequence;
messages_done += messages.length; messages_done += messages.length;
this.progress = {name: 'messages', value: messages_done, max: messages_max}; this.progress = {
name: 'messages',
value: messages_done,
max: messages_max,
};
} else { } else {
break; break;
} }
@ -122,7 +222,8 @@ class TfSneakerAppElement extends LitElement {
FROM messages FROM messages
JOIN messages_refs ON messages.id = messages_refs.message JOIN messages_refs ON messages.id = messages_refs.message
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`, WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
[id]); [id]
);
let blobs_done = 0; let blobs_done = 0;
for (let row of blobs) { for (let row of blobs) {
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length}; this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
@ -133,7 +234,10 @@ class TfSneakerAppElement extends LitElement {
console.log(`Failed to get ${row.id}: ${e.message}`); console.log(`Failed to get ${row.id}: ${e.message}`);
} }
if (blob) { if (blob) {
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob)); zip.file(
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
new Uint8Array(blob)
);
} }
blobs_done++; blobs_done++;
} }
@ -181,7 +285,11 @@ class TfSneakerAppElement extends LitElement {
continue; continue;
} }
let message = JSON.parse(line); let message = JSON.parse(line);
this.progress = {name: 'messages', value: progress++, max: total_messages}; this.progress = {
name: 'messages',
value: progress++,
max: total_messages,
};
if (await tfrpc.rpc.store_message(message.value)) { if (await tfrpc.rpc.store_message(message.value)) {
success.messages++; success.messages++;
} }
@ -202,7 +310,13 @@ class TfSneakerAppElement extends LitElement {
let progress; let progress;
if (this.progress) { if (this.progress) {
if (this.progress.max) { 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>`; progress = html`<div>
<label for="progress">${this.progress.name}</label
><progress
value=${this.progress.value}
max=${this.progress.max}
></progress>
</div>`;
} else { } else {
progress = html`<div><span>${this.progress.name}</span></div>`; progress = html`<div><span>${this.progress.name}</span></div>`;
} }
@ -218,13 +332,17 @@ class TfSneakerAppElement extends LitElement {
<input type="text" id="search" @keypress=${this.keypress}></input> <input type="text" id="search" @keypress=${this.keypress}></input>
<input type="button" value="Search Users" @click=${this.search}></input> <input type="button" value="Search Users" @click=${this.search}></input>
<ul> <ul>
${Object.entries(this.feeds).map(([id, name]) => html` ${Object.entries(this.feeds).map(
([id, name]) => html`
<li> <li>
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`} ${this.progress
? undefined
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
${name} ${name}
<code style="color: #ccc">${id}</code> <code style="color: #ccc">${id}</code>
</li> </li>
`)} `
)}
</ul> </ul>
`; `;
} }

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&DUxMMCJcuhm6S9jg/eKgEyWodkITu6Tg9g5I5wgLWFU=.sha256" "previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.sha256"
} }

View File

@ -109,7 +109,7 @@ core.register('onConnectionsChanged', async function() {
}); });
async function main() { async function main() {
if (typeof(database) !== 'undefined') { if (typeof database !== 'undefined') {
g_database = await database('ssb'); g_database = await database('ssb');
} }
await app.setDocument(utf8Decode(await getFile('index.html'))); await app.setDocument(utf8Decode(await getFile('index.html')));

View File

@ -72,9 +72,11 @@ export function picker(callback, anchor) {
list.appendChild(header); list.appendChild(header);
let any = false; let any = false;
for (let entry of Object.entries(row[1])) { for (let entry of Object.entries(row[1])) {
if (search && if (
search &&
search.length && search.length &&
entry[0].toLowerCase().indexOf(search) == -1) { entry[0].toLowerCase().indexOf(search) == -1
) {
continue; continue;
} }
let emoji = document.createElement('span'); let emoji = document.createElement('span');

View File

@ -1,8 +1,8 @@
<!DOCTYPE html> <!doctype html>
<html style="color: #fff"> <html style="color: #fff">
<head> <head>
<title>Tilde Friends</title> <title>Tilde Friends</title>
<base target="_top"> <base target="_top" />
<link rel="stylesheet" href="tribute.css" /> <link rel="stylesheet" href="tribute.css" />
<style> <style>
.tribute-container { .tribute-container {
@ -12,7 +12,9 @@
</head> </head>
<body style="background-color: #223a5e"> <body style="background-color: #223a5e">
<tf-app class="w3-deep-purple" /> <tf-app class="w3-deep-purple" />
<script>window.litDisableBundleWarning = true;</script> <script>
window.litDisableBundleWarning = true;
</script>
<script src="filesaver.min.js"></script> <script src="filesaver.min.js"></script>
<script src="commonmark.min.js"></script> <script src="commonmark.min.js"></script>
<script src="commonmark-linkify.js" type="module"></script> <script src="commonmark-linkify.js" type="module"></script>

View File

@ -34,9 +34,13 @@ class TfElement extends LitElement {
this.users = {}; this.users = {};
this.loaded = false; this.loaded = false;
this.tags = []; this.tags = [];
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; }); tfrpc.rpc.getBroadcasts().then((b) => {
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; }); self.broadcasts = b || [];
tfrpc.rpc.getHash().then(hash => self.set_hash(hash)); });
tfrpc.rpc.getConnections().then((c) => {
self.connections = c || [];
});
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) { tfrpc.register(function hashChanged(hash) {
self.set_hash(hash); self.set_hash(hash);
}); });
@ -86,9 +90,14 @@ class TfElement extends LitElement {
last_row_id: 0, last_row_id: 0,
}; };
} }
let max_row_id = (await tfrpc.rpc.query(` let max_row_id = (
await tfrpc.rpc.query(
`
SELECT MAX(rowid) AS max_row_id FROM messages SELECT MAX(rowid) AS max_row_id FROM messages
`, []))[0].max_row_id; `,
[]
)
)[0].max_row_id;
for (let id of Object.keys(cache.about)) { for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) { if (ids.indexOf(id) == -1) {
delete cache.about[id]; delete cache.about[id];
@ -98,7 +107,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query( let abouts = await tfrpc.rpc.query(
` `
SELECT SELECT
messages.* messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM FROM
messages, messages,
json_each(?1) AS following json_each(?1) AS following
@ -109,7 +118,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about' json_extract(messages.content, '$.type') = 'about'
UNION UNION
SELECT SELECT
messages.* messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM FROM
messages, messages,
json_each(?2) AS following json_each(?2) AS following
@ -120,17 +129,21 @@ class TfElement extends LitElement {
ORDER BY messages.author, messages.sequence ORDER BY messages.author, messages.sequence
`, `,
[ [
JSON.stringify(ids.filter(id => cache.about[id])), JSON.stringify(ids.filter((id) => cache.about[id])),
JSON.stringify(ids.filter(id => !cache.about[id])), JSON.stringify(ids.filter((id) => !cache.about[id])),
cache.last_row_id, cache.last_row_id,
max_row_id, max_row_id,
]); ]
);
for (let about of abouts) { for (let about of abouts) {
let content = JSON.parse(about.content); let content = JSON.parse(about.content);
if (content.about === about.author) { if (content.about === about.author) {
delete content.type; delete content.type;
delete content.about; delete content.about;
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content); cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
} }
} }
cache.last_row_id = max_row_id; cache.last_row_id = max_row_id;
@ -145,15 +158,13 @@ class TfElement extends LitElement {
async fetch_new_message(id) { async fetch_new_message(id) {
let messages = await tfrpc.rpc.query( let messages = await tfrpc.rpc.query(
` `
SELECT messages.* SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.id = ? WHERE messages.id = ?
`, `,
[ [JSON.stringify(this.following), id]
JSON.stringify(this.following), );
id,
]);
if (messages && messages.length) { if (messages && messages.length) {
this.unread = [...this.unread, ...messages]; this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024); this.unread = this.unread.slice(this.unread.length - 1024);
@ -173,7 +184,7 @@ class TfElement extends LitElement {
} }
async create_identity() { async create_identity() {
if (confirm("Are you sure you want to create a new identity?")) { if (confirm('Are you sure you want to create a new identity?')) {
await tfrpc.rpc.createIdentity(); await tfrpc.rpc.createIdentity();
this.ids = (await tfrpc.rpc.getIdentities()) || []; this.ids = (await tfrpc.rpc.getIdentities()) || [];
if (this.ids && !this.whoami) { if (this.ids && !this.whoami) {
@ -185,17 +196,32 @@ class TfElement extends LitElement {
render_id_picker() { render_id_picker() {
return html` return html`
<div style="display: flex; gap: 8px"> <div style="display: flex; gap: 8px">
<tf-id-picker id="picker" style="flex: 1 1 auto" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker> <tf-id-picker
<button class="w3-button w3-dark-grey w3-border" style="flex: 0 0 auto" @click=${this.create_identity} id="create_identity">Create Identity</button> id="picker"
style="flex: 1 1 auto"
selected=${this.whoami}
.ids=${this.ids}
.users=${this.users}
@change=${this._handle_whoami_changed}
></tf-id-picker>
<button
class="w3-button w3-dark-grey w3-border"
style="flex: 0 0 auto"
@click=${this.create_identity}
id="create_identity"
>
Create Identity
</button>
</div> </div>
`; `;
} }
async load_recent_tags() { async load_recent_tags() {
let start = new Date(); let start = new Date();
this.tags = await tfrpc.rpc.query(` this.tags = await tfrpc.rpc.query(
`
WITH WITH
recent AS (SELECT id, content FROM messages recent AS (SELECT id, json(content) AS content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post' WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024), ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
@ -207,7 +233,9 @@ class TfElement extends LitElement {
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions), combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined) by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10 SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`, [new Date() - 7 * 24 * 60 * 60 * 1000]); `,
[new Date() - 7 * 24 * 60 * 60 * 1000]
);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds'); console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
} }
@ -241,23 +269,53 @@ class TfElement extends LitElement {
let users = this.users; let users = this.users;
if (this.tab === 'news') { if (this.tab === 'news') {
return html` return html`
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news> <tf-tab-news
id="tf-tab-news"
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {
return html` return html`
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections> <tf-tab-connections
.users=${this.users}
.connections=${this.connections}
.broadcasts=${this.broadcasts}
></tf-tab-connections>
`; `;
} else if (this.tab === 'mentions') { } else if (this.tab === 'mentions') {
return html` return html`
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions> <tf-tab-mentions
.following=${this.following}
whoami=${this.whoami}
.users="${this.users}}"
></tf-tab-mentions>
`; `;
} else if (this.tab === 'search') { } else if (this.tab === 'search') {
return html` return html`
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search> <tf-tab-search
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
></tf-tab-search>
`; `;
} else if (this.tab === 'query') { } else if (this.tab === 'query') {
return html` return html`
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query> <tf-tab-query
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#sql=')
? decodeURIComponent(this.hash.substring(5))
: null}
></tf-tab-query>
`; `;
} }
} }
@ -295,21 +353,32 @@ class TfElement extends LitElement {
let tabs = html` let tabs = html`
<div class="w3-bar w3-black"> <div class="w3-bar w3-black">
${Object.entries(k_tabs).map(([k, v]) => html` ${Object.entries(k_tabs).map(
<button title=${v} class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab == v ? 'w3-red' : 'w3-black'}" @click=${() => self.set_tab(v)}>${k}</button> ([k, v]) => html`
`)} <button
title=${v}
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
v
? 'w3-red'
: 'w3-black'}"
@click=${() => self.set_tab(v)}
>
${k}
</button>
`
)}
</div> </div>
`; `;
let contents = let contents = !this.loaded
!this.loaded ? ? this.loading
this.loading ? ? html`<div>Loading...</div>`
html`<div>Loading...</div>` : : html`<div>Select or create an identity.</div>`
html`<div>Select or create an identity.</div>` : : this.render_tab();
this.render_tab();
return html` return html`
${this.render_id_picker()} ${this.render_id_picker()} ${tabs}
${tabs} ${this.tags.map(
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)} (x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents} ${contents}
`; `;
} }

View File

@ -58,7 +58,9 @@ class TfComposeElement extends LitElement {
link: link, link: link,
}; };
} }
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name; draft.mentions[link].name = name.startsWith('@')
? name.substring(1)
: name;
updated = true; updated = true;
} }
if (updated) { if (updated) {
@ -72,27 +74,32 @@ class TfComposeElement extends LitElement {
let preview = this.renderRoot.getElementById('preview'); let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value); preview.innerHTML = this.process_text(edit.value);
let content_warning = this.renderRoot.getElementById('content_warning'); let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview'); let content_warning_preview = this.renderRoot.getElementById(
'content_warning_preview'
);
if (content_warning && content_warning_preview) { if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value; content_warning_preview.innerText = content_warning.value;
} }
} }
notify(draft) { notify(draft) {
this.dispatchEvent(new CustomEvent('tf-draft', { this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { detail: {
id: this.branch, id: this.branch,
draft: draft draft: draft,
}, },
})); })
);
} }
change() { change() {
let draft = this.get_draft(); let draft = this.get_draft();
draft.text = this.renderRoot.getElementById('edit')?.value; draft.text = this.renderRoot.getElementById('edit')?.value;
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value; draft.content_warning =
this.renderRoot.getElementById('content_warning')?.value;
this.notify(draft); this.notify(draft);
} }
@ -109,13 +116,17 @@ class TfComposeElement extends LitElement {
let context = canvas.getContext('2d'); let context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height); context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data_url = canvas.toDataURL(mime_type); let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0)); let result = atob(data_url.split(',')[1])
.split('')
.map((x) => x.charCodeAt(0));
resolve(result); resolve(result);
}; };
img.onerror = function (event) { img.onerror = function (event) {
reject(new Error('Failed to load image.')); reject(new Error('Failed to load image.'));
}; };
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join(''); let raw = Array.from(new Uint8Array(buffer))
.map((b) => String.fromCharCode(b))
.join('');
let original = `data:${type};base64,${btoa(raw)}`; let original = `data:${type};base64,${btoa(raw)}`;
img.src = original; img.src = original;
}); });
@ -131,7 +142,11 @@ class TfComposeElement extends LitElement {
let best_buffer; let best_buffer;
let best_type; let best_type;
for (let format of ['image/png', 'image/jpeg', 'image/webp']) { for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
let test_buffer = await self.convert_to_format(buffer, file.type, format); let test_buffer = await self.convert_to_format(
buffer,
file.type,
format
);
if (!best_buffer || test_buffer.length < best_buffer.length) { if (!best_buffer || test_buffer.length < best_buffer.length) {
best_buffer = test_buffer; best_buffer = test_buffer;
best_type = format; best_type = format;
@ -201,7 +216,11 @@ class TfComposeElement extends LitElement {
to = [...to]; to = [...to];
message.recps = to; message.recps = to;
console.log('message is now', message); console.log('message is now', message);
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message)); message = await tfrpc.rpc.encrypt(
this.whoami,
to,
JSON.stringify(message)
);
console.log('encrypted as', message); console.log('encrypted as', message);
} }
try { try {
@ -241,12 +260,15 @@ class TfComposeElement extends LitElement {
this.last_autocomplete = text; this.last_autocomplete = text;
let results = []; let results = [];
try { try {
let rows = await tfrpc.rpc.query(` let rows = await tfrpc.rpc.query(
SELECT messages.content FROM messages_fts(?) `
SELECT json(messages.content) FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ? WHERE messages.content LIKE ?
ORDER BY timestamp DESC LIMIT 10 ORDER BY timestamp DESC LIMIT 10
`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]); `,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
);
for (let row of rows) { for (let row of rows) {
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) { for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) { if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
@ -265,7 +287,10 @@ class TfComposeElement extends LitElement {
let tribute = new Tribute({ let tribute = new Tribute({
collection: [ collection: [
{ {
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})), values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) { selectTemplate: function (item) {
return `[@${item.original.key}](${item.original.value})`; return `[@${item.original.key}](${item.original.value})`;
}, },
@ -293,7 +318,10 @@ class TfComposeElement extends LitElement {
let encrypt = this.renderRoot.getElementById('encrypt_to'); let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) { if (encrypt) {
let tribute = new Tribute({ let tribute = new Tribute({
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})), values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) { selectTemplate: function (item) {
return item.original.value; return item.original.value;
}, },
@ -311,17 +339,27 @@ class TfComposeElement extends LitElement {
render_mention(mention) { render_mention(mention) {
let self = this; let self = this;
return html` return html` <div style="display: flex; flex-direction: row">
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em"> <div style="align-self: center; margin: 0.5em">
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button> <button
class="w3-button w3-dark-grey"
title="Remove ${mention.name} mention"
@click=${() => self.remove_mention(mention.link)}
>
🚮
</button>
</div> </div>
<div style="display: flex; flex-direction: column"> <div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3> <h3>${mention.name}</h3>
<div style="padding-left: 1em"> <div style="padding-left: 1em">
${Object.entries(mention) ${Object.entries(mention)
.filter(x => x[0] != 'name') .filter((x) => x[0] != 'name')
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)} .map(
(x) =>
html`<div>
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
</div>`
)}
</div> </div>
</div> </div>
</div>`; </div>`;
@ -359,10 +397,19 @@ class TfComposeElement extends LitElement {
return html` return html`
<div class="w3-card-4 w3-margin w3-padding"> <div class="w3-card-4 w3-margin w3-padding">
<select id="select" class="w3-select w3-dark-grey"> <select id="select" class="w3-select w3-dark-grey">
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)} ${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>`
)}
</select> </select>
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button> <button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button> Attach
</button>
<button
class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)}
>
Cancel
</button>
</div> </div>
`; `;
} }
@ -374,9 +421,16 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps(); self.apps = await tfrpc.rpc.apps();
} }
if (!this.apps) { if (!this.apps) {
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>Attach App</button>`; return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>
Attach App
</button>`;
} else { } else {
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`; return html`<button
class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)}
>
Discard App
</button>`;
} }
} }
@ -435,11 +489,13 @@ class TfComposeElement extends LitElement {
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button> <button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div> </div>
<ul> <ul>
${draft.encrypt_to.map(x => html` ${draft.encrypt_to.map(
(x) => html`
<li> <li>
<tf-user id=${x} .users=${this.users}></tf-user> <tf-user id=${x} .users=${this.users}></tf-user>
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input> <input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
</li>`)} </li>`
)}
</ul> </ul>
`; `;
} }
@ -455,34 +511,65 @@ class TfComposeElement extends LitElement {
let self = this; let self = this;
let draft = self.get_draft(); let draft = self.get_draft();
let content_warning = let content_warning =
draft.content_warning !== undefined ? draft.content_warning !== undefined
html`<div class="w3-panel w3-round-xlarge w3-blue"> ? html`<div class="w3-panel w3-round-xlarge w3-blue">
<p id="content_warning_preview">${draft.content_warning}</p> <p id="content_warning_preview">${draft.content_warning}</p>
</div>` : </div>`
undefined; : undefined;
let encrypt = draft.encrypt_to !== undefined ? let encrypt =
undefined : draft.encrypt_to !== undefined
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`; ? undefined
: html`<button
class="w3-button w3-dark-grey"
@click=${() => this.set_encrypt([])}
>
🔐
</button>`;
let result = html` let result = html`
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box"> <div
class="w3-card-4 w3-blue-grey w3-padding"
style="box-sizing: border-box"
>
${this.render_encrypt()} ${this.render_encrypt()}
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px"> <div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
<div style="flex: 1 0 50%"> <div style="flex: 1 0 50%">
<p><textarea class="w3-input w3-dark-grey w3-border" style="resize: vertical" placeholder="Write a post here." id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste}>${draft.text}</textarea></p> <p>
<textarea
class="w3-input w3-dark-grey w3-border"
style="resize: vertical"
placeholder="Write a post here."
id="edit"
@input=${this.input}
@change=${this.change}
@paste=${this.paste}
>
${draft.text}</textarea
>
</p>
</div> </div>
<div style="flex: 1 0 50%"> <div style="flex: 1 0 50%">
${content_warning} ${content_warning}
<div id="preview"></div> <div id="preview"></div>
</div> </div>
</div> </div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))} ${Object.values(draft.mentions || {}).map((x) =>
${this.render_attach_app()} self.render_mention(x)
${this.render_content_warning()} )}
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button> ${this.render_attach_app()} ${this.render_content_warning()}
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button> <button
${this.render_attach_app_button()} class="w3-button w3-dark-grey"
${encrypt} id="submit"
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button> @click=${this.submit}
>
Submit
</button>
<button class="w3-button w3-dark-grey" @click=${this.attach}>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-dark-grey" @click=${this.discard}>
Discard
</button>
</div> </div>
`; `;
return result; return result;

View File

@ -24,15 +24,28 @@ class TfIdentityPickerElement extends LitElement {
changed(event) { changed(event) {
this.selected = event.srcElement.value; this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', { this.dispatchEvent(
new Event('change', {
srcElement: this, srcElement: this,
})); })
);
} }
render() { render() {
return html` return html`
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden"> <select
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${this.users[id]?.name ? (this.users[id]?.name + ' - ') : undefined}<small>${id}</small></option>`)} class="w3-select w3-dark-grey w3-padding w3-border"
@change=${this.changed}
style="max-width: 100%; overflow: hidden"
>
${(this.ids ?? []).map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${this.users[id]?.name
? this.users[id]?.name + ' - '
: undefined}<small>${id}</small>
</option>`
)}
</select> </select>
`; `;
} }

View File

@ -31,14 +31,27 @@ class TfMessageElement extends LitElement {
} }
show_reply() { show_reply() {
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: { let event = new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.message?.id,
draft: {
encrypt_to: this.message?.decrypted?.recps, encrypt_to: this.message?.decrypted?.recps,
}}}); },
},
});
this.dispatchEvent(event); this.dispatchEvent(event);
} }
discard_reply() { discard_reply() {
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}})); this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {id: this.id, draft: undefined},
})
);
} }
render_votes() { render_votes() {
@ -53,12 +66,19 @@ class TfMessageElement extends LitElement {
return expression; return expression;
} }
} }
return html`<div>${(this.message.votes || []).map( return html`<div>
vote => html` ${(this.message.votes || []).map(
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}"> (vote) => html`
<span
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
vote.timestamp
)}"
>
${normalize_expression(vote.content.vote.expression)} ${normalize_expression(vote.content.vote.expression)}
</span> </span>
`)}</div>`; `
)}
</div>`;
} }
render_raw() { render_raw() {
@ -72,30 +92,40 @@ class TfMessageElement extends LitElement {
content: this.message?.content, content: this.message?.content,
signature: this.message?.signature, 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) { vote(emoji) {
let reaction = emoji; let reaction = emoji;
let message = this.message.id; let message = this.message.id;
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) { if (
tfrpc.rpc.appendMessage( confirm(
this.whoami, 'Are you sure you want to react with ' +
{ reaction +
' to ' +
message +
'?'
)
) {
tfrpc.rpc
.appendMessage(this.whoami, {
type: 'vote', type: 'vote',
vote: { vote: {
link: message, link: message,
value: 1, value: 1,
expression: reaction, expression: reaction,
}, },
}).catch(function(error) { })
.catch(function (error) {
alert(error?.message); alert(error?.message);
}); });
} }
} }
react(event) { react(event) {
emojis.picker(x => this.vote(x)); emojis.picker((x) => this.vote(x));
} }
show_image(link) { show_image(link) {
@ -129,7 +159,10 @@ class TfMessageElement extends LitElement {
body_click(event) { body_click(event) {
if (event.srcElement.tagName == 'IMG') { if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src); this.show_image(event.srcElement.src);
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) { } else if (
event.srcElement.tagName == 'DIV' &&
event.srcElement.classList.contains('img_caption')
) {
let next = event.srcElement.nextSibling; let next = event.srcElement.nextSibling;
if (next.style.display == 'block') { if (next.style.display == 'block') {
next.style.display = 'none'; next.style.display = 'none';
@ -140,50 +173,77 @@ class TfMessageElement extends LitElement {
} }
render_mention(mention) { render_mention(mention) {
if (!mention?.link || typeof(mention.link) != 'string') { if (!mention?.link || typeof mention.link != 'string') {
return html` <pre>${JSON.stringify(mention)}</pre>`; return html` <pre>${JSON.stringify(mention)}</pre>`;
} else if (mention?.link?.startsWith('&') && } else if (
mention?.type?.startsWith('image/')) { mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')
) {
return html` return html`
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}> <img
src=${'/' + mention.link + '/view'}
style="max-width: 128px; max-height: 128px"
title=${mention.name}
@click=${() => this.show_image('/' + mention.link + '/view')}
/>
`; `;
} else if (mention.link?.startsWith('&') && } else if (
mention.name?.startsWith('audio:')) { mention.link?.startsWith('&') &&
mention.name?.startsWith('audio:')
) {
return html` return html`
<audio controls style="height: 32px"> <audio controls style="height: 32px">
<source src=${'/' + mention.link + '/view'}></source> <source src=${'/' + mention.link + '/view'}></source>
</audio> </audio>
`; `;
} else if (mention.link?.startsWith('&') && } else if (
mention.name?.startsWith('video:')) { mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')
) {
return html` return html`
<video controls style="max-height: 240px; max-width: 128px"> <video controls style="max-height: 240px; max-width: 128px">
<source src=${'/' + mention.link + '/view'}></source> <source src=${'/' + mention.link + '/view'}></source>
</video> </video>
`; `;
} else if (mention.link?.startsWith('&') && } else if (
mention?.type === 'application/tildefriends') { mention.link?.startsWith('&') &&
mention?.type === 'application/tildefriends'
) {
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`; return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) { } else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`; return html` <a href=${'#' + encodeURIComponent(mention.link)}
>${mention.name}</a
>`;
} else if (mention.link?.startsWith('#')) { } else if (mention.link?.startsWith('#')) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`; return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) { >${mention.link}</a
>`;
} else if (
Object.keys(mention).length == 2 &&
mention.link &&
mention.name
) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`; return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else { } else {
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`; return html` <pre style="white-space: pre-wrap">
${JSON.stringify(mention, null, 2)}</pre
>`;
} }
} }
render_mentions() { render_mentions() {
let mentions = this.message?.content?.mentions || []; let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1); mentions = mentions.filter(
(x) => this.message?.content?.text?.indexOf(x.link) === -1
);
if (mentions.length) { if (mentions.length) {
let self = this; let self = this;
return html` return html`
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"> <fieldset
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
>
<legend>Mentions</legend> <legend>Mentions</legend>
${mentions.map(x => self.render_mention(x))} ${mentions.map((x) => self.render_mention(x))}
</fieldset> </fieldset>
`; `;
} }
@ -194,28 +254,55 @@ class TfMessageElement extends LitElement {
return 0; return 0;
} }
let total = message.child_messages.length; let total = message.child_messages.length;
for (let m of message.child_messages) for (let m of message.child_messages) {
{
total += this.total_child_messages(m); total += this.total_child_messages(m);
} }
return total; return total;
} }
set_expanded(expanded, tag) { set_expanded(expanded, tag) {
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}})); this.dispatchEvent(
new CustomEvent('tf-expand', {
bubbles: true,
composed: true,
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
})
);
} }
toggle_expanded(tag) { toggle_expanded(tag) {
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag); this.set_expanded(
!this.expanded[(this.message.id || '') + (tag || '')],
tag
);
} }
render_children() { render_children() {
let self = this; let self = this;
if (this.message.child_messages?.length) { if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) { if (!this.expanded[this.message.id]) {
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`; return html`<button
class="w3-button w3-dark-grey"
@click=${() => self.set_expanded(true)}
>
+ ${this.total_child_messages(this.message) + ' More'}
</button>`;
} else { } else {
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(false)}>Collapse</button>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`; return html`<button
class="w3-button w3-dark-grey"
@click=${() => self.set_expanded(false)}
>
Collapse</button
>${(this.message.child_messages || []).map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>`
)}`;
} }
} }
} }
@ -231,13 +318,12 @@ class TfMessageElement extends LitElement {
} }
if (Array.isArray(content.mentions)) { if (Array.isArray(content.mentions)) {
for (let mention of content.mentions) { for (let mention of content.mentions) {
if (typeof mention?.link === 'string' && if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
mention.link.startsWith('#')) {
channels.push(mention.link); channels.push(mention.link);
} }
} }
} }
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`); return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
} }
render() { render() {
@ -250,52 +336,108 @@ class TfMessageElement extends LitElement {
switch (this.format) { switch (this.format) {
case 'raw': case 'raw':
if (content?.type == 'post' || content?.type == 'blog') { if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'md')}
>
Markdown
</button>`;
} else { } else {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
} }
break; break;
case 'md': case 'md':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
break; break;
case 'decrypted': case 'decrypted':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
break; break;
default: default:
if (this.message.decrypted) { if (this.message.decrypted) {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'decrypted')}
>
Decrypted
</button>`;
} else { } else {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`; raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
} }
break; break;
} }
function small_frame(inner) { function small_frame(inner) {
let body; let body;
return html` return html`
<div class="w3-card-4" style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"> <div
class="w3-card-4"
style="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> <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> <span style="padding-right: 8px"
${raw_button} ><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
${self.format == 'raw' ? self.render_raw() : inner} self.message.timestamp
).toLocaleString()}</span
>
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()} ${self.render_votes()}
</div> </div>
`; `;
} }
if (this.message?.type === 'contact_group') { if (this.message?.type === 'contact_group') {
return html` return html` <div
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"> class="w3-card-4"
${this.message.messages.map(x => style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>` >
${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>`; </div>`;
} else if (this.message.placeholder) { } else if (this.message.placeholder) {
return html` return html` <div
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"> class="w3-card-4"
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder) 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)
<div>${this.render_votes()}</div> <div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(x => html` ${(this.message.child_messages || []).map(
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message> (x) => html`
`)} <tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>
`
)}
</div>`; </div>`;
} else if (typeof (content?.type === 'string')) { } else if (typeof (content?.type === 'string')) {
if (content.type == 'about') { if (content.type == 'about') {
@ -307,7 +449,7 @@ class TfMessageElement extends LitElement {
} }
if (content.image !== undefined) { if (content.image !== undefined) {
image = html` image = html`
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div> <div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
`; `;
} }
if (content.description !== undefined) { if (content.description !== undefined) {
@ -317,41 +459,54 @@ class TfMessageElement extends LitElement {
</div> </div>
`; `;
} }
let update = content.about == this.message.author ? let update =
html`<div style="font-weight: bold">Updated profile.</div>` : content.about == this.message.author
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`; ? html`<div style="font-weight: bold">Updated profile.</div>`
return small_frame(html` : html`<div style="font-weight: bold">
${update} Updated profile for
${name} <tf-user id=${content.about} .users=${this.users}></tf-user>.
${image} </div>`;
${description} return small_frame(html` ${update} ${name} ${image} ${description} `);
`);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return html` return html`
<div> <div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
is is
${ ${content.blocking === true
content.blocking === true ? 'blocking' : ? 'blocking'
content.blocking === false ? 'no longer blocking' : : content.blocking === false
content.following === true ? 'following' : ? 'no longer blocking'
content.following === false ? 'no longer following' : : content.following === true
'?' ? 'following'
} : content.following === false
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user> ? 'no longer following'
: '?'}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
</div> </div>
`; `;
} else if (content.type == 'post') { } else if (content.type == 'post') {
let reply = (this.drafts[this.message?.id] !== undefined) ? html` let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose <tf-compose
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
root=${this.message.content.root || this.message.id} root=${content.root || this.message.id}
branch=${this.message.id} branch=${this.message.id}
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose> @tf-discard=${this.discard_reply}
` : html` ></tf-compose>
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button> `
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`; `;
let self = this; let self = this;
let body; let body;
@ -360,35 +515,47 @@ class TfMessageElement extends LitElement {
body = this.render_raw(); body = this.render_raw();
break; break;
case 'md': case 'md':
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`; body = html`<code
style="white-space: pre-wrap; overflow-wrap: anywhere"
>${content.text}</code
>`;
break; break;
case 'message': case 'message':
body = unsafeHTML(tfutils.markdown(content.text)); body = unsafeHTML(tfutils.markdown(content.text));
break; break;
case 'decrypted': case 'decrypted':
body = html`<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${JSON.stringify(content, null, 2)}</pre>`; body = html`<pre
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break; break;
} }
let content_warning = html` let content_warning = html`
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div> <div
class="w3-panel w3-round-xlarge w3-blue"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
</div>
`; `;
let content_html = let content_html = html`
html`
${this.render_channels()} ${this.render_channels()}
<div @click=${this.body_click}>${body}</div> <div @click=${this.body_click}>${body}</div>
${this.render_mentions()} ${this.render_mentions()}
`; `;
let payload = let payload = content.contentWarning
content.contentWarning ? ? self.expanded[(this.message.id || '') + ':cw']
self.expanded[(this.message.id || '') + ':cw'] ? ? html` ${content_warning} ${content_html} `
html` : content_warning
${content_warning} : content_html;
${content_html} let is_encrypted = this.message?.decrypted
` : ? html`<span style="align-self: center">🔓</span>`
content_warning : : undefined;
content_html; let style_background = this.message?.decrypted
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined; ? 'rgba(255, 0, 0, 0.2)'
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)'; : 'rgba(255, 255, 255, 0.1)';
return html` return html`
<style> <style>
code { code {
@ -404,26 +571,37 @@ class TfMessageElement extends LitElement {
display: block; display: block;
} }
</style> </style>
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"> <div
class="w3-card-4"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted} ${is_encrypted}
<span style="flex: 1"></span> <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 style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span> <span>${raw_button}</span>
</div> </div>
${payload} ${payload} ${this.render_votes()}
${this.render_votes()}
<p> <p>
${reply} ${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button> <button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p> </p>
${this.render_children()} ${this.render_children()}
</div> </div>
`; `;
} else if (content.type === 'issue') { } else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined; let is_encrypted = this.message?.decrypted
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)'; ? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)';
return html` return html`
<style> <style>
code { code {
@ -439,18 +617,25 @@ class TfMessageElement extends LitElement {
display: block; display: block;
} }
</style> </style>
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"> <div
class="w3-card-4"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted} ${is_encrypted}
<span style="flex: 1"></span> <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 style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span> <span>${raw_button}</span>
</div> </div>
${content.text} ${content.text} ${this.render_votes()}
${this.render_votes()}
<p> <p>
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button> <button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p> </p>
${this.render_children()} ${this.render_children()}
</div> </div>
@ -460,10 +645,13 @@ class TfMessageElement extends LitElement {
tfrpc.rpc.get_blob(content.blog).then(function (data) { tfrpc.rpc.get_blob(content.blog).then(function (data) {
self.blog_data = data; self.blog_data = data;
}); });
let payload = let payload = this.expanded[(this.message.id || '') + ':blog']
this.expanded[(this.message.id || '') + ':blog'] ? ? html`<div>
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` : ${this.blog_data
undefined; ? unsafeHTML(tfutils.markdown(this.blog_data))
: 'Loading...'}
</div>`
: undefined;
let body; let body;
switch (this.format) { switch (this.format) {
case 'raw': case 'raw':
@ -476,7 +664,7 @@ class TfMessageElement extends LitElement {
body = html` body = html`
<div <div
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer" style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
@click=${x => self.toggle_expanded(':blog')}> @click=${(x) => self.toggle_expanded(':blog')}>
<h2>${content.title}</h2> <h2>${content.title}</h2>
<div style="display: flex; flex-direction: row"> <div style="display: flex; flex-direction: row">
<img src=/${content.thumbnail}/view></img> <img src=/${content.thumbnail}/view></img>
@ -487,16 +675,25 @@ class TfMessageElement extends LitElement {
`; `;
break; break;
} }
let reply = (this.drafts[this.message?.id] !== undefined) ? html` let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose <tf-compose
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
root=${this.message.content.root || this.message.id} root=${content.root || this.message.id}
branch=${this.message.id} branch=${this.message.id}
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose> @tf-discard=${this.discard_reply}
` : html` ></tf-compose>
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button> `
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`; `;
return html` return html`
<style> <style>
@ -513,11 +710,17 @@ class TfMessageElement extends LitElement {
display: block; display: block;
} }
</style> </style>
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"> <div
class="w3-card-4"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row"> <div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span> <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 style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span> <span>${raw_button}</span>
</div> </div>
@ -525,37 +728,52 @@ class TfMessageElement extends LitElement {
${this.render_mentions()} ${this.render_mentions()}
<div> <div>
${reply} ${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button> <button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</div> </div>
${this.render_votes()} ${this.render_votes()} ${this.render_children()}
${this.render_children()}
</div> </div>
`; `;
} else if (content.type === 'pub') { } else if (content.type === 'pub') {
return small_frame(html` return small_frame(
<style> html` <style>
span { span {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
</style> </style>
<span> <span>
<div> <div>
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user> 🍻
<tf-user
.users=${this.users}
id=${content.address.key}
></tf-user>
</div> </div>
<pre>${content.address.host}:${content.address.port}</pre> <pre>${content.address.host}:${content.address.port}</pre>
</span>`); </span>`
);
} else if (content.type === 'channel') { } else if (content.type === 'channel') {
return small_frame(html` return small_frame(html`
<div> <div>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a> ${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a
>
</div> </div>
`); `);
} else if (typeof(this.message.content) == 'string') { } else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) { if (this.message?.decrypted) {
if (this.format == 'decrypted') { if (this.format == 'decrypted') {
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`); return small_frame(
html`<span>🔓</span>
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
);
} else { } else {
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`); return small_frame(
html`<span>🔓</span>
<div>${this.message.decrypted.type}</div>`
);
} }
} else { } else {
return small_frame(html`<span>🔒</span>`); return small_frame(html`<span>🔒</span>`);

View File

@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.vote.link; message.parent_message = message.content.vote.link;
} else if (message.content.type == 'post') { } else if (message.content.type == 'post') {
if (message.content.root) { if (message.content.root) {
if (typeof(message.content.root) === 'string') { if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root); let m = ensure_message(message.content.root);
if (!m.child_messages) { if (!m.child_messages) {
m.child_messages = []; m.child_messages = [];
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
for (let message of messages) { for (let message of messages) {
try { try {
message.content = JSON.parse(message.content); message.content = JSON.parse(message.content);
} catch { } catch {}
}
if (!messages_by_id[message.id]) { if (!messages_by_id[message.id]) {
messages_by_id[message.id] = message; messages_by_id[message.id] = message;
link_message(message); link_message(message);
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
message.parent_message = placeholder.parent_message; message.parent_message = placeholder.parent_message;
message.child_messages = placeholder.child_messages; message.child_messages = placeholder.child_messages;
message.votes = placeholder.votes; message.votes = placeholder.votes;
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) { if (
let children = messages_by_id[placeholder.parent_message].child_messages; placeholder.parent_message &&
messages_by_id[placeholder.parent_message]
) {
let children =
messages_by_id[placeholder.parent_message].child_messages;
children.splice(children.indexOf(placeholder), 1); children.splice(children.indexOf(placeholder), 1);
children.push(message); children.push(message);
} }
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
let latest = 0; let latest = 0;
for (let message of messages || []) { for (let message of messages || []) {
if (message.latest_subtree_timestamp === undefined) { if (message.latest_subtree_timestamp === undefined) {
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages)); message.latest_subtree_timestamp = Math.max(
message.timestamp ?? 0,
this.update_latest_subtree_timestamp(message.child_messages)
);
} }
latest = Math.max(latest, message.latest_subtree_timestamp); latest = Math.max(latest, message.latest_subtree_timestamp);
} }
@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
function recursive_sort(messages, top) { function recursive_sort(messages, top) {
if (messages) { if (messages) {
if (top) { if (top) {
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp); messages.sort(
(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
);
} else { } else {
messages.sort((a, b) => a.timestamp - b.timestamp); messages.sort((a, b) => a.timestamp - b.timestamp);
} }
for (let message of messages) { for (let message of messages) {
recursive_sort(message.child_messages, false); recursive_sort(message.child_messages, false);
} }
return messages.map(x => Object.assign({}, x)); return messages.map((x) => Object.assign({}, x));
} else { } else {
return {}; return {};
} }
} }
let roots = Object.values(messages_by_id).filter(x => !x.parent_message); let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
this.update_latest_subtree_timestamp(roots); this.update_latest_subtree_timestamp(roots);
return recursive_sort(roots, true); return recursive_sort(roots, true);
} }
@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
load_and_render(messages) { load_and_render(messages) {
let messages_by_id = this.process_messages(messages); let messages_by_id = this.process_messages(messages);
let final_messages = this.group_following(this.finalize_messages(messages_by_id)); let final_messages = this.group_following(
this.finalize_messages(messages_by_id)
);
return html` return html`
<div style="display: flex; flex-direction: column"> <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>`)} ${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>`
)}
</div> </div>
`; `;
} }

View File

@ -36,23 +36,29 @@ class TfProfileElement extends LitElement {
this.following = undefined; this.following = undefined;
this.blocking = undefined; this.blocking = undefined;
let result = await tfrpc.rpc.query(` let result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following SELECT json_extract(content, '$.following') AS following
FROM messages WHERE author = ? AND FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND json_extract(content, '$.contact') = ? AND
following IS NOT NULL following IS NOT NULL
ORDER BY sequence DESC LIMIT 1 ORDER BY sequence DESC LIMIT 1
`, [this.whoami, this.id]); `,
[this.whoami, this.id]
);
this.following = result?.[0]?.following ?? false; this.following = result?.[0]?.following ?? false;
result = await tfrpc.rpc.query(` result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.blocking') AS blocking SELECT json_extract(content, '$.blocking') AS blocking
FROM messages WHERE author = ? AND FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND json_extract(content, '$.contact') = ? AND
blocking IS NOT NULL blocking IS NOT NULL
ORDER BY sequence DESC LIMIT 1 ORDER BY sequence DESC LIMIT 1
`, [this.whoami, this.id]); `,
[this.whoami, this.id]
);
this.blocking = result?.[0]?.blocking ?? false; this.blocking = result?.[0]?.blocking ?? false;
} }
} }
@ -60,13 +66,16 @@ class TfProfileElement extends LitElement {
async initial_load() { async initial_load() {
this.server_follows_me = undefined; this.server_follows_me = undefined;
let server_id = await tfrpc.rpc.getServerIdentity(); let server_id = await tfrpc.rpc.getServerIdentity();
let followed = await tfrpc.rpc.query(` let followed = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following SELECT json_extract(content, '$.following') AS following
FROM messages FROM messages
WHERE author = ? AND WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1 json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`, [server_id, this.whoami]); `,
[server_id, this.whoami]
);
let is_followed = false; let is_followed = false;
for (let row of followed) { for (let row of followed) {
is_followed = row.following != 0; is_followed = row.following != 0;
@ -75,11 +84,18 @@ class TfProfileElement extends LitElement {
} }
modify(change) { modify(change) {
tfrpc.rpc.appendMessage(this.whoami, tfrpc.rpc
Object.assign({ .appendMessage(
this.whoami,
Object.assign(
{
type: 'contact', type: 'contact',
contact: this.id, contact: this.id,
}, change)).catch(function(error) { },
change
)
)
.catch(function (error) {
alert(error?.message); alert(error?.message);
}); });
} }
@ -122,9 +138,12 @@ class TfProfileElement extends LitElement {
message[key] = this.editing[key]; message[key] = this.editing[key];
} }
} }
tfrpc.rpc.appendMessage(this.whoami, message).then(function() { tfrpc.rpc
.appendMessage(this.whoami, message)
.then(function () {
self.editing = null; self.editing = null;
}).catch(function(error) { })
.catch(function (error) {
alert(error?.message); alert(error?.message);
}); });
} }
@ -139,13 +158,17 @@ class TfProfileElement extends LitElement {
input.type = 'file'; input.type = 'file';
input.onchange = function (event) { input.onchange = function (event) {
let file = event.target.files[0]; let file = event.target.files[0];
file.arrayBuffer().then(function(buffer) { file
.arrayBuffer()
.then(function (buffer) {
let bin = Array.from(new Uint8Array(buffer)); let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin); return tfrpc.rpc.store_blob(bin);
}).then(function(id) { })
.then(function (id) {
self.editing = Object.assign({}, self.editing, {image: id}); self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing); console.log(self.editing);
}).catch(function(e) { })
.catch(function (e) {
alert(e.message); alert(e.message);
}); });
}; };
@ -166,15 +189,22 @@ class TfProfileElement extends LitElement {
} }
render() { render() {
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) { if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load(); this.initial_load();
} }
this.load(); this.load();
let self = this; let self = this;
let profile = this.users[this.id] || {}; let profile = this.users[this.id] || {};
tfrpc.rpc.query( tfrpc.rpc
.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`, `SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]).then(function(result) { [this.id]
)
.then(function (result) {
self.size = result[0].size; self.size = result[0].size;
}); });
let edit; let edit;
@ -184,52 +214,75 @@ class TfProfileElement extends LitElement {
if (this.editing) { if (this.editing) {
let server_follow; let server_follow;
if (this.server_follows_me === true) { if (this.server_follows_me === true) {
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`; server_follow = html`<button
class="w3-button w3-dark-grey"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) { } else if (this.server_follows_me === false) {
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`; server_follow = html`<button
class="w3-button w3-dark-grey"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
} }
edit = html` edit = html`
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button> <button class="w3-button w3-dark-grey" @click=${this.save_edits}>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button> Save Profile
</button>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
Discard
</button>
${server_follow} ${server_follow}
`; `;
} else { } else {
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`; edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
Edit Profile
</button>`;
} }
} }
if (this.id !== this.whoami && if (this.id !== this.whoami && this.following !== undefined) {
this.following !== undefined) { follow = this.following
follow = ? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
this.following ? Unfollow
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` : </button>`
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`; : html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
Follow
</button>`;
} }
if (this.id !== this.whoami && if (this.id !== this.whoami && this.blocking !== undefined) {
this.blocking !== undefined) { block = this.blocking
block = ? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
this.blocking ? Unblock
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` : </button>`
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`; : html`<button class="w3-button w3-dark-grey" @click=${this.block}>
Block
</button>`;
} }
let edit_profile = this.editing ? html` let edit_profile = this.editing
? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px"> <div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div class="w3-container"> <div class="w3-container">
<div> <div>
<label for="name">Name:</label> <label for="name">Name:</label>
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input> <input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
</div> </div>
<div><label for="description">Description:</label></div> <div><label for="description">Description:</label></div>
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea> <textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" 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> <label for="public_web_hosting">Public Web Hosting:</label>
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input> <input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
</div> </div>
<div> <div>
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button> <button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
</div> </div>
</div> </div>
</div>` : null; </div>`
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link; : null;
let image =
typeof profile.image == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image; image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description; let description = this.editing?.description ?? profile.description;
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px"> return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">

File diff suppressed because it is too large Load Diff

View File

@ -43,10 +43,12 @@ class TfTabConnectionsElement extends LitElement {
render_room_peers(connection) { render_room_peers(connection) {
let self = this; let self = this;
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection); let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
if (peers.length) { if (peers.length) {
let connections = this.connections.map(x => x.id); let connections = this.connections.map((x) => x.id);
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`; return html`${peers
.filter((x) => connections.indexOf(x.pubkey) == -1)
.map((x) => html`${self.render_room_peer(x)}`)}`;
} }
} }
@ -58,7 +60,12 @@ class TfTabConnectionsElement extends LitElement {
let self = this; let self = this;
return html` return html`
<li> <li>
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button> <button
class="w3-button w3-dark-grey"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡 <tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
</li> </li>
`; `;
@ -67,7 +74,12 @@ class TfTabConnectionsElement extends LitElement {
render_broadcast(connection) { render_broadcast(connection) {
return html` return html`
<li> <li>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button> <button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.connect(connection)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> <tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)} ${this.render_connection_summary(connection)}
</li> </li>
@ -81,11 +93,20 @@ class TfTabConnectionsElement extends LitElement {
render_connection(connection) { render_connection(connection) {
return html` return html`
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button> <button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
<tf-user id=${connection.id} .users=${this.users}></tf-user> <tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`} ${connection.tunnel !== undefined
? '🚇'
: html`(${connection.host}:${connection.port})`}
<ul> <ul>
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)} ${this.connections
.filter((x) => x.tunnel === this.connections.indexOf(connection))
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)} ${this.render_room_peers(connection.id)}
</ul> </ul>
`; `;
@ -97,30 +118,54 @@ class TfTabConnectionsElement extends LitElement {
<div class="w3-container"> <div class="w3-container">
<h2>New Connection</h2> <h2>New Connection</h2>
<textarea class="w3-input w3-dark-grey" id="code"></textarea> <textarea class="w3-input w3-dark-grey" id="code"></textarea>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button> <button
class="w3-button w3-dark-grey"
@click=${() =>
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
>
Connect
</button>
<h2>Broadcasts</h2> <h2>Broadcasts</h2>
<ul> <ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))} ${this.broadcasts
.filter((x) => x.address)
.map((x) => self.render_broadcast(x))}
</ul> </ul>
<h2>Connections</h2> <h2>Connections</h2>
<ul> <ul>
${this.connections.filter(x => x.tunnel === undefined).map(x => html` ${this.connections
<li>${this.render_connection(x)}</li> .filter((x) => x.tunnel === undefined)
`)} .map((x) => html` <li>${this.render_connection(x)}</li> `)}
</ul> </ul>
<h2>Stored Connections (WIP)</h2> <h2>Stored Connections (WIP)</h2>
<ul> <ul>
${this.stored_connections.map(x => html` ${this.stored_connections.map(
(x) => html`
<li> <li>
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button> <button
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button> class="w3-button w3-dark-grey"
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user> @click=${() => self.forget_stored_connection(x)}
>
Forget
</button>
<button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.connect(x)}
>
Connect
</button>
${x.address}:${x.port}
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li> </li>
`)} `
)}
</ul> </ul>
<h2>Local Accounts</h2> <h2>Local Accounts</h2>
<ul> <ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)} ${this.identities.map(
(x) =>
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
)}
</ul> </ul>
</div> </div>
`; `;

View File

@ -27,15 +27,21 @@ class TfTabMentionsElement extends LitElement {
async load() { async load() {
console.log('Loading...', this.whoami); console.log('Loading...', this.whoami);
let results = await tfrpc.rpc.query(` let results = await tfrpc.rpc.query(
SELECT messages.* `
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?) FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.author != ? WHERE messages.author != ?
ORDER BY timestamp DESC limit 20 ORDER BY timestamp DESC limit 20
`, `,
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]); [
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
this.whoami,
]
);
console.log('Done.'); console.log('Done.');
this.messages = results; this.messages = results;
} }
@ -58,7 +64,14 @@ class TfTabMentionsElement extends LitElement {
this.load(); this.load();
} }
return html` return html`
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news> <tf-news
id="news"
whoami=${this.whoami}
.messages=${this.messages}
.users=${this.users}
.expanded=${this.expanded}
@tf-expand=${this.on_expand}
></tf-news>
`; `;
} }
} }

View File

@ -33,54 +33,53 @@ class TfTabNewsFeedElement extends LitElement {
if (this.hash.startsWith('#@')) { if (this.hash.startsWith('#@')) {
let r = await tfrpc.rpc.query( let r = await tfrpc.rpc.query(
` `
WITH mine AS (SELECT messages.* WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE messages.author = ? WHERE messages.author = ?
ORDER BY sequence DESC ORDER BY sequence DESC
LIMIT 20) LIMIT 20)
SELECT messages.* SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mine FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT * FROM mine SELECT * FROM mine
`, `,
[ [this.hash.substring(1)]
this.hash.substring(1), );
]);
return r; return r;
} else if (this.hash.startsWith('#%')) { } else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query( return await tfrpc.rpc.query(
` `
SELECT messages.* SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE id = ?1 WHERE id = ?1
UNION UNION
SELECT messages.* SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages JOIN messages_refs FROM messages JOIN messages_refs
ON messages.id = messages_refs.message ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1 WHERE messages_refs.ref = ?1
`, `,
[ [this.hash.substring(1)]
this.hash.substring(1), );
]);
} else { } else {
let promises = []; let promises = [];
const k_following_limit = 256; const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) { for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(tfrpc.rpc.query( promises.push(
tfrpc.rpc.query(
` `
WITH news AS (SELECT messages.* WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages FROM messages
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? AND messages.timestamp < ? WHERE messages.timestamp > ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC) ORDER BY messages.timestamp DESC)
SELECT messages.* SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.ref JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT messages.* SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.message JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id JOIN messages ON messages_refs.ref = messages.id
@ -95,7 +94,9 @@ class TfTabNewsFeedElement extends LitElement {
** messages with far-future timestamps from staying at the top forever. ** messages with far-future timestamps from staying at the top forever.
*/ */
new Date().valueOf() + 24 * 60 * 60 * 1000, new Date().valueOf() + 24 * 60 * 60 * 1000,
])); ]
)
);
} }
return [].concat(...(await Promise.all(promises))); return [].concat(...(await Promise.all(promises)));
} }
@ -106,29 +107,26 @@ class TfTabNewsFeedElement extends LitElement {
this.start_time = last_start_time - 24 * 60 * 60 * 1000; this.start_time = last_start_time - 24 * 60 * 60 * 1000;
let more = await tfrpc.rpc.query( let more = await tfrpc.rpc.query(
` `
WITH news AS (SELECT messages.* WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages FROM messages
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? WHERE messages.timestamp > ?
AND messages.timestamp <= ? AND messages.timestamp <= ?
ORDER BY messages.timestamp DESC) ORDER BY messages.timestamp DESC)
SELECT messages.* SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.ref JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT messages.* SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news FROM news
JOIN messages_refs ON news.id = messages_refs.message JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id JOIN messages ON messages_refs.ref = messages.id
UNION UNION
SELECT news.* FROM news SELECT news.* FROM news
`, `,
[ [JSON.stringify(this.following), this.start_time, last_start_time]
JSON.stringify(this.following), );
this.start_time,
last_start_time,
]);
this.messages = await this.decrypt([...more, ...this.messages]); this.messages = await this.decrypt([...more, ...this.messages]);
} }
@ -139,14 +137,12 @@ class TfTabNewsFeedElement extends LitElement {
let content; let content;
try { try {
content = JSON.parse(message?.content); content = JSON.parse(message?.content);
} catch { } catch {}
} if (typeof content === 'string') {
if (typeof(content) === 'string') {
let decrypted; let decrypted;
try { try {
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content); decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
} catch { } catch {}
}
if (decrypted) { if (decrypted) {
try { try {
message.decrypted = JSON.parse(decrypted); message.decrypted = JSON.parse(decrypted);
@ -165,18 +161,25 @@ class TfTabNewsFeedElement extends LitElement {
} }
render() { render() {
if (!this.messages || if (
!this.messages ||
this._messages_hash !== this.hash || this._messages_hash !== this.hash ||
this._messages_following !== this.following) { this._messages_following !== this.following
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`); ) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
let self = this; let self = this;
this.messages = []; this.messages = [];
this._messages_hash = this.hash; this._messages_hash = this.hash;
this._messages_following = this.following; this._messages_following = this.following;
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) { this.fetch_messages()
.then(this.decrypt.bind(this))
.then(function (messages) {
self.messages = messages; self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`); console.log(`loading mesages done for ${self.whoami}`);
}).catch(function(error) { })
.catch(function (error) {
alert(JSON.stringify(error, null, 2)); alert(JSON.stringify(error, null, 2));
}); });
} }
@ -184,12 +187,22 @@ class TfTabNewsFeedElement extends LitElement {
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) { if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html` more = html`
<p> <p>
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button> <button class="w3-button w3-dark-grey" @click=${this.load_more}>
Load More
</button>
</p> </p>
`; `;
} }
return html` 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> <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} ${more}
`; `;
} }

View File

@ -48,7 +48,9 @@ class TfTabNewsElement extends LitElement {
let news = this.shadowRoot?.getElementById('news'); let news = this.shadowRoot?.getElementById('news');
if (news) { if (news) {
console.log('injecting messages', news.messages); console.log('injecting messages', news.messages);
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x])))); news.add_messages(
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
);
this.dispatchEvent(new CustomEvent('refresh')); this.dispatchEvent(new CustomEvent('refresh'));
} }
} }
@ -62,11 +64,16 @@ class TfTabNewsElement extends LitElement {
let type = 'private'; let type = 'private';
try { try {
type = JSON.parse(message.content).type || type; type = JSON.parse(message.content).type || type;
} catch { } catch {}
}
counts[type] = (counts[type] || 0) + 1; counts[type] = (counts[type] || 0) + 1;
} }
return '↻ Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', '); return (
'↻ Show New: ' +
Object.keys(counts)
.sort()
.map((x) => counts[x].toString() + ' ' + x + 's')
.join(', ')
);
} }
draft(event) { draft(event) {
@ -96,23 +103,52 @@ class TfTabNewsElement extends LitElement {
} }
on_keypress(event) { on_keypress(event) {
if (event.target === document.body && if (event.target === document.body && event.key == '.') {
event.key == '.') {
this.show_more(); this.show_more();
} }
} }
render() { render() {
let profile = this.hash.startsWith('#@') ? let profile = this.hash.startsWith('#@')
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined; ? html`<tf-profile
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
return html` return html`
<p class="w3-bar"> <p class="w3-bar">
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button> <button
class="w3-bar-item w3-button w3-dark-grey"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
</p> </p>
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div> <div>
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div> Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
></tf-compose>
</div>
${profile} ${profile}
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed> <tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
></tf-tab-news-feed>
`; `;
} }
} }

View File

@ -41,7 +41,7 @@ class TfTabQueryElement extends LitElement {
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query)); await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
let start_time = new Date(); let start_time = new Date();
try { try {
this.results = await tfrpc.rpc.query(query, []) this.results = await tfrpc.rpc.query(query, []);
} catch (error) { } catch (error) {
this.error = error; this.error = error;
} }
@ -79,8 +79,15 @@ class TfTabQueryElement extends LitElement {
} else { } else {
let keys = Object.keys(this.results[0]).sort(); let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%"> return html`<table style="width: 100%; max-width: 100%">
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr> <tr>
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)} ${keys.map((key) => html`<th>${key}</th>`)}
</tr>
${this.results.map(
(row) =>
html`<tr>
${keys.map((key) => html`<td>${row[key]}</td>`)}
</tr>`
)}
</table>`; </table>`;
} }
} }
@ -100,13 +107,28 @@ class TfTabQueryElement extends LitElement {
let self = this; let self = this;
return html` return html`
<div style="display: flex; flex-direction: row; gap: 4px"> <div style="display: flex; flex-direction: row; gap: 4px">
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea> <textarea
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button> id="search"
rows="8"
class="w3-input w3-dark-grey"
style="flex: 1; resize: vertical"
@keydown=${this.search_keydown}
>
${this.query}</textarea
>
<button
class="w3-button w3-dark-grey"
@click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)}
>
Execute
</button>
</div>
<div ?hidden=${this.duration === undefined}>
Took ${this.duration / 1000.0} seconds.
</div> </div>
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div> <div ?hidden=${this.duration !== undefined}>Executing...</div>
${this.render_error()} ${this.render_error()} ${this.render_results()}
${this.render_results()}
`; `;
} }
} }

View File

@ -33,14 +33,16 @@ class TfTabSearchElement extends LitElement {
search.select(); search.select();
} }
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query)); await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
let results = await tfrpc.rpc.query(` let results = await tfrpc.rpc.query(
SELECT messages.* `
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?) FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100 ORDER BY timestamp DESC limit 100
`, `,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]); ['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.'); console.log('Done.');
search = this.renderRoot.getElementById('search'); search = this.renderRoot.getElementById('search');
if (search) { if (search) {

View File

@ -17,7 +17,11 @@ class TfTagElement extends LitElement {
render() { render() {
let number = this.count ? html` (${this.count})` : undefined; let number = this.count ? html` (${this.count})` : undefined;
return html`<a href="#q=${this.tag}" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px">${this.tag}${number}</a>`; return html`<a
href="#q=${this.tag}"
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
>${this.tag}${number}</a
>`;
} }
} }

View File

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

View File

@ -2,20 +2,32 @@ import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js'; import * as hashtagify from './commonmark-hashtag.js';
function image(node, entering) { function image(node, entering) {
if (node.firstChild?.type === 'text' && if (
node.firstChild.literal.startsWith('video:')) { node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) { if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>'); 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.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1; this.disableTags += 1;
} else { } else {
this.disableTags -= 1; this.disableTags -= 1;
this.lit('</video>'); this.lit('</video>');
} }
} else if (node.firstChild?.type === 'text' && } else if (
node.firstChild.literal.startsWith('audio:')) { node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) { if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>'); 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.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1; this.disableTags += 1;
} else { } else {
@ -25,7 +37,11 @@ function image(node, entering) {
} else { } else {
if (entering) { if (entering) {
if (this.disableTags === 0) { if (this.disableTags === 0) {
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>'); this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) { if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="'); this.lit('<img src="" alt="');
} else { } else {
@ -58,14 +74,20 @@ export function markdown(md) {
node = event.node; node = event.node;
if (event.entering) { if (event.entering) {
if (node.type == 'link') { if (node.type == 'link') {
if (node.destination.startsWith('@') && if (
node.destination.endsWith('.ed25519')) { node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + node.destination; node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') && } else if (
node.destination.endsWith('.sha256')) { node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + node.destination; node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') && } else if (
node.destination.endsWith('.sha256')) { node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view'; node.destination = '/' + node.destination + '/view';
} }
} else if (node.type == 'image') { } else if (node.type == 'image') {

View File

@ -27,7 +27,8 @@ async function todo_add(list) {
let set = new Set(names); let set = new Set(names);
set.add(list); set.add(list);
names = JSON.stringify([...set].sort()); names = JSON.stringify([...set].sort());
exchanged = original === names || await g_db.exchange('files', original, names); exchanged =
original === names || (await g_db.exchange('files', original, names));
} }
return exchanged; return exchanged;
} }
@ -42,7 +43,8 @@ async function todo_remove(list) {
let set = new Set(names); let set = new Set(names);
set.delete(list); set.delete(list);
names = JSON.stringify([...set].sort()); names = JSON.stringify([...set].sort());
exchanged = original === names || await g_db.exchange('files', original, names); exchanged =
original === names || (await g_db.exchange('files', original, names));
} }
await g_db.remove('list:' + list); await g_db.remove('list:' + list);
return exchanged; return exchanged;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>TODO</title> <title>TODO</title>

View File

@ -4,7 +4,7 @@ import * as tfrpc from '/static/tfrpc.js';
class TodosElement extends LitElement { class TodosElement extends LitElement {
static get properties() { static get properties() {
return { return {
lists: {type: Array} lists: {type: Array},
}; };
} }
@ -12,9 +12,12 @@ class TodosElement extends LitElement {
super(); super();
this.lists = []; this.lists = [];
let self = this; let self = this;
tfrpc.rpc.todo_get_all().then(function(lists) { tfrpc.rpc
.todo_get_all()
.then(function (lists) {
self.lists = lists; self.lists = lists;
}).catch(function(error) { })
.catch(function (error) {
console.log(error); console.log(error);
}); });
} }
@ -32,9 +35,15 @@ class TodosElement extends LitElement {
return html` return html`
<div> <div>
<div style="display: flex"> <div style="display: flex">
${this.lists.map(x => html` ${this.lists.map(
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list> (x) => html`
`)} <tf-todo-list
name=${x.name}
.items=${x.items}
@change=${this.refresh}
></tf-todo-list>
`
)}
</div> </div>
<input type="button" @click=${this.new_list} value="+ List"></input> <input type="button" @click=${this.new_list} value="+ List"></input>
</div>`; </div>`;
@ -59,16 +68,22 @@ class TodoListElement extends LitElement {
save() { save() {
let self = this; let self = this;
console.log('saving', self.name, self.items); console.log('saving', self.name, self.items);
tfrpc.rpc.todo_set(self.name, self.items).then(function() { tfrpc.rpc
.todo_set(self.name, self.items)
.then(function () {
console.log('saved', self.name, self.items); console.log('saved', self.name, self.items);
}).catch(function(error) { })
.catch(function (error) {
console.log(error); console.log(error);
}); });
} }
remove_item(item) { remove_item(item) {
let index = this.items.indexOf(item); let index = this.items.indexOf(item);
this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1)); this.items = [].concat(
this.items.slice(0, index),
this.items.slice(index + 1)
);
this.save(); this.save();
} }
@ -106,20 +121,20 @@ class TodoListElement extends LitElement {
let self = this; let self = this;
if (index === this.editing) { if (index === this.editing) {
return html` return html`
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input> <div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
<input <input
id="edit" id="edit"
type="text" type="text"
value=${item.text} value=${item.text}
@change=${event => self.input_change(event, item)} @change=${(event) => self.input_change(event, item)}
@keydown=${event => self.input_keydown(event, item)} @keydown=${(event) => self.input_keydown(event, item)}
@blur=${x => self.input_blur(item)}></input> @blur=${(x) => self.input_blur(item)}></input>
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div> <span @click=${(x) => self.remove_item(item)} style="cursor: pointer">❎</span></div>
`; `;
} else { } else {
return html` return html`
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input> <div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
<span @click=${x => self.editing = index}>${item.text || '(empty)'}</span> <span @click=${(x) => (self.editing = index)}>${item.text || '(empty)'}</span>
`; `;
} }
} }
@ -139,10 +154,13 @@ class TodoListElement extends LitElement {
rename(new_name) { rename(new_name) {
let self = this; let self = this;
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() { return tfrpc.rpc
.todo_rename(this.name, new_name)
.then(function () {
self.dispatchEvent(new Event('change')); self.dispatchEvent(new Event('change'));
self.editing_name = false; self.editing_name = false;
}).catch(function(error) { })
.catch(function (error) {
console.log(error); console.log(error);
alert(error.message); alert(error.message);
self.editing_name = false; self.editing_name = false;
@ -163,19 +181,25 @@ class TodoListElement extends LitElement {
render() { render() {
let self = this; let self = this;
let name = this.editing_name ? let name = this.editing_name
html`<input ? html`<input
type="text" type="text"
id="edit" id="edit"
@keydown=${event => self.name_keydown(event)} @keydown=${(event) => self.name_keydown(event)}
@blur=${event => self.name_blur(event.srcElement.value)} @blur=${(event) => self.name_blur(event.srcElement.value)}
value=${this.name}></input>` : value=${this.name}></input>`
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`; : html`<h2 @click=${(x) => (this.editing_name = true)}>${this.name}</h2>`;
return html` return html`
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"> <div
style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"
>
${name} ${name}
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))} ${(this.items || [])
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))} .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.add_item}>+ Item</button>
<button @click=${self.remove_list}>- List</button> <button @click=${self.remove_list}>- List</button>
</div> </div>

View File

@ -1,23 +1,36 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="w3.css"> <link rel="stylesheet" href="w3.css" />
<link rel="stylesheet" href="fontawesome.min.css"> <link rel="stylesheet" href="fontawesome.min.css" />
<link rel="stylesheet" href="regular.min.css"> <link rel="stylesheet" href="regular.min.css" />
<link rel="stylesheet" href="solid.min.css"> <link rel="stylesheet" href="solid.min.css" />
<link rel="stylesheet" href="brands.min.css"> <link rel="stylesheet" href="brands.min.css" />
<style> <style>
body,h1,h2,h3,h4,h5 {font-family: "Poppins", sans-serif} body,
body {font-size: 16px;} h1,
img {margin-bottom: -8px;} h2,
.mySlides {display: none;} h3,
h4,
h5 {
font-family: 'Poppins', sans-serif;
}
body {
font-size: 16px;
}
img {
margin-bottom: -8px;
}
.mySlides {
display: none;
}
</style> </style>
<base target="_top"> <base target="_top" />
</head> </head>
<body class="w3-content w3-black" style="max-width:1500px;"> <body class="w3-content w3-black" style="max-width: 1500px">
<!-- The App Section --> <!-- The App Section -->
<div class="w3-padding-64 w3-white"> <div class="w3-padding-64 w3-white">
<div class="w3-row-padding"> <div class="w3-row-padding">
@ -25,20 +38,34 @@
<h1 class="w3-jumbo"> <h1 class="w3-jumbo">
<b>😎 Tilde Friends</b> <b>😎 Tilde Friends</b>
</h1> </h1>
<h1 class="w3-xxlarge w3-text-green"><b>Make apps and friends from the comfort of your web browser.</b></h1> <h1 class="w3-xxlarge w3-text-green">
<p>Tilde Friends is a platform for building, running, and sharing web applications.</p> <b>Make apps and friends from the comfort of your web browser.</b>
<p>Available for lots of devices: </h1>
<p>
Tilde Friends is a platform for building, running, and sharing web
applications.
</p>
<p>
Available for lots of devices:
<i class="fa-brands fa-linux w3-xlarge"></i> <i class="fa-brands fa-linux w3-xlarge"></i>
<i class="fa-brands fa-android w3-xlarge"></i> <i class="fa-brands fa-android w3-xlarge"></i>
<i class="fa-brands fa-apple w3-xlarge"></i> <i class="fa-brands fa-apple w3-xlarge"></i>
<i class="fa fa-mobile-screen w3-xlarge"></i> <i class="fa fa-mobile-screen w3-xlarge"></i>
<i class="fa-brands fa-windows w3-xlarge"></i> <i class="fa-brands fa-windows w3-xlarge"></i>
</p> </p>
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a> <a
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try It</a> class="w3-button w3-black w3-padding-large"
href="https://www.tildefriends.net/~cory/releases/"
><i class="fa fa-download"></i> Download</a
>
<a
class="w3-button w3-black w3-padding-large"
href="https://www.tildefriends.net/~cory/apps/"
><i class="fa fa-link"></i> Try It</a
>
</div> </div>
<div class="w3-col l4 m6"> <div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small"> <img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
</div> </div>
</div> </div>
</div> </div>
@ -47,19 +74,28 @@
<div class="w3-light-grey"> <div class="w3-light-grey">
<div class="w3-row-padding w3-padding-64"> <div class="w3-row-padding w3-padding-64">
<div class="w3-col l4 m6 s4"> <div class="w3-col l4 m6 s4">
<a href="https://scuttlebutt.nz/"><img class="w3-image w3-round-large" src="ssb.png" alt="Secure Scuttlebutt"></a> <a href="https://scuttlebutt.nz/"
><img
class="w3-image w3-round-large"
src="ssb.png"
alt="Secure Scuttlebutt"
/></a>
</div> </div>
<div class="w3-col l8 m6" style="height: auto"> <div class="w3-col l8 m6" style="height: auto">
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1> <h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
<p> <p>
Tilde Friends participates in the <a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed social network. Tilde Friends participates in the
<a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed
social network.
</p> </p>
<p> <p>
Share apps with friends. Discover new apps made by enemies. Post pictures of your coffee. Or just lurk. Share apps with friends. Discover new apps made by enemies. Post
pictures of your coffee. Or just lurk.
</p> </p>
<p> <p>
The social network integration provides tools for connecting with other people world-wide The social network integration provides tools for connecting with
while still allowing apps and everything to operate offline. other people world-wide while still allowing apps and everything to
operate offline.
</p> </p>
</div> </div>
</div> </div>
@ -70,14 +106,16 @@
<div class="w3-row-padding"> <div class="w3-row-padding">
<div class="w3-col l8 m6"> <div class="w3-col l8 m6">
<h1 class="w3-jumbo"><b>Edit Anything</b></h1> <h1 class="w3-jumbo"><b>Edit Anything</b></h1>
<i class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray" style="padding: 32px"></i> <i
class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray"
style="padding: 32px"
></i>
<p> <p>
See that <code><b>edit</b></code> link near the top left corner of this page? It's there for See that <code><b>edit</b></code> link near the top left corner of
every Tilde Friends app, so you can modify and see your changes right away. this page? It's there for every Tilde Friends app, so you can modify
</p> and see your changes right away.
<p>
It's kind of like a wiki, but for code!
</p> </p>
<p>It's kind of like a wiki, but for code!</p>
</div> </div>
</div> </div>
</div> </div>
@ -86,16 +124,22 @@
<div class="w3-padding-64 w3-grey"> <div class="w3-padding-64 w3-grey">
<div class="w3-row-padding"> <div class="w3-row-padding">
<div class="w3-col"> <div class="w3-col">
<h1 class="w3-jumbo" style="text-align: right"><b>Sandbox Security</b></h1> <h1 class="w3-jumbo" style="text-align: right">
<i class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" style="padding: 32px"></i> <b>Sandbox Security</b>
</h1>
<i
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
style="padding: 32px"
></i>
<p> <p>
Tilde Friends tries to make sure apps can be trusted using similar techniques to how web Tilde Friends tries to make sure apps can be trusted using similar
browsers and operating systems do it. techniques to how web browsers and operating systems do it.
</p> </p>
<p> <p>
This is all a work in progress, and it varies by platform, so don't give it all your This is all a work in progress, and it varies by platform, so don't
innermost secrets yet, but do kick its tires and give it all your innermost secrets yet, but do kick its tires and
<a href="mailto:cory@tildefriends.net">share</a> any surprises you find. <a href="mailto:cory@tildefriends.net">share</a> any surprises you
find.
</p> </p>
</div> </div>
</div> </div>
@ -105,10 +149,16 @@
<div class="w3-container w3-padding-64 w3-light-grey w3-center"> <div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1> <h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
<p>Tilde Friends is built using boring, trusted tech.</p> <p>Tilde Friends is built using boring, trusted tech.</p>
<p>Though of course for building Tilde Friends apps, you are free to use whatever fits.</p> <p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits.
</p>
<div class="w3-row" style="margin-top: 64px"> <div class="w3-row" style="margin-top: 64px">
<a href="https://en.wikipedia.org/wiki/C_(programming_language)" class="w3-col s3"> <a
href="https://en.wikipedia.org/wiki/C_(programming_language)"
class="w3-col s3"
>
<i class="fa fa-c w3-text-blue w3-jumbo"></i> <i class="fa fa-c w3-text-blue w3-jumbo"></i>
<p>C</p> <p>C</p>
</a> </a>
@ -139,7 +189,10 @@
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i> <i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
<p>OpenSSL</p> <p>OpenSSL</p>
</a> </a>
<a href="https://github.com/ianlancetaylor/libbacktrace" class="w3-col s3"> <a
href="https://github.com/ianlancetaylor/libbacktrace"
class="w3-col s3"
>
<i class="fa fa-burst w3-text-pink w3-jumbo"></i> <i class="fa fa-burst w3-text-pink w3-jumbo"></i>
<p>libbacktrace</p> <p>libbacktrace</p>
</a> </a>
@ -167,7 +220,10 @@
<!-- Footer --> <!-- Footer -->
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge"> <footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
<p class="w3-medium">This page and Tilde Friends itself was made by Cory mostly in coffee shops and a local pizza place.</p> <p class="w3-medium">
This page and Tilde Friends itself was made by Cory mostly in coffee
shops and a local pizza place.
</p>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "📝", "emoji": "📝",
"previous": "&/wl8HE2jZShRXTYEVYRrK3pjHwi41Wbxl9HoSJaQP6Y=.sha256" "previous": "&DnfuAUGzzalSh9NgZXnzDc9Ru5aM0omfRJ4h27jYw4k=.sha256"
} }

View File

@ -11,10 +11,13 @@ function markdown(md) {
let node = event.node; let node = event.node;
if (event.entering) { if (event.entering) {
if (node.destination?.startsWith('&')) { if (node.destination?.startsWith('&')) {
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal; node.destination =
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (node.type === 'link') { } else if (node.type === 'link') {
if (node.destination.indexOf(':') == -1 && if (
node.destination.indexOf('/') == -1) { node.destination.indexOf(':') == -1 &&
node.destination.indexOf('/') == -1
) {
node.destination = `${node.destination}`; node.destination = `${node.destination}`;
} }
} }
@ -29,7 +32,9 @@ async function main() {
let wiki_name = request.path.substring(0, slash); let wiki_name = request.path.substring(0, slash);
let wiki_doc_name = request.path.substring(slash + 1); let wiki_doc_name = request.path.substring(slash + 1);
let ids = Object.keys(await ssb.following(await ssb.getOwnerIdentities(), 1)); let ids = Object.keys(
await ssb.following(await ssb.getOwnerIdentities(), 1)
);
let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {}); let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {});
let wiki; let wiki;
for (let w of Object.values(wikis)) { for (let w of Object.values(wikis)) {
@ -40,7 +45,13 @@ async function main() {
} }
let wiki_doc; let wiki_doc;
if (wiki) { if (wiki) {
let [max_row_id, wiki_docs] = await utils.collection(ids, 'wiki-doc', wiki.id, -1, {}); let [max_row_id, wiki_docs] = await utils.collection(
ids,
'wiki-doc',
wiki.id,
-1,
{}
);
for (let w of Object.values(wiki_docs)) { for (let w of Object.values(wiki_docs)) {
if (w.name === wiki_doc_name && !w.tombstone) { if (w.name === wiki_doc_name && !w.tombstone) {
wiki_doc = w; wiki_doc = w;

View File

@ -1,11 +1,14 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<base target="_top"> <base target="_top" />
<link rel="stylesheet" href="tildefriends.css" />
</head> </head>
<body style="color: #fff"> <body>
<tf-collections-app></tf-collections-app> <tf-collections-app></tf-collections-app>
<script>window.litDisableBundleWarning = true;</script> <script>
window.litDisableBundleWarning = true;
</script>
<script src="tf-collection.js" type="module"></script> <script src="tf-collection.js" type="module"></script>
<script src="tf-id-picker.js" type="module"></script> <script src="tf-id-picker.js" type="module"></script>
<script src="tf-wiki-doc.js" type="module"></script> <script src="tf-wiki-doc.js" type="module"></script>

View File

@ -5,6 +5,7 @@ class TfCollectionElement extends LitElement {
static get properties() { static get properties() {
return { return {
whoami: {type: String}, whoami: {type: String},
category: {type: String},
collection: {type: Object}, collection: {type: Object},
selected_id: {type: String}, selected_id: {type: String},
is_creating: {type: Boolean}, is_creating: {type: Boolean},
@ -14,79 +15,100 @@ class TfCollectionElement extends LitElement {
on_create(event) { on_create(event) {
let name = this.shadowRoot.getElementById('create_name').value; let name = this.shadowRoot.getElementById('create_name').value;
this.dispatchEvent(new CustomEvent('create', { this.dispatchEvent(
new CustomEvent('create', {
bubbles: true, bubbles: true,
detail: { detail: {
name: name, name: name,
}, },
})); })
);
this.is_creating = false; this.is_creating = false;
} }
on_rename(event) { on_rename(event) {
let id = this.shadowRoot.getElementById('select').value; let id = this.shadowRoot.getElementById('select').value;
let name = this.shadowRoot.getElementById('rename_name').value; let name = this.shadowRoot.getElementById('rename_name').value;
this.dispatchEvent(new CustomEvent('rename', { this.dispatchEvent(
new CustomEvent('rename', {
bubbles: true, bubbles: true,
detail: { detail: {
id: id, id: id,
value: this.collection[id], value: this.collection[id],
name: name, name: name,
}, },
})); })
);
this.is_renaming = false; this.is_renaming = false;
} }
on_tombstone(event) { on_tombstone(event) {
let id = this.shadowRoot.getElementById('select').value; let id = this.shadowRoot.getElementById('select').value;
if (confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)) { if (
this.dispatchEvent(new CustomEvent('tombstone', { confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)
) {
this.dispatchEvent(
new CustomEvent('tombstone', {
bubbles: true, bubbles: true,
detail: { detail: {
id: id, id: id,
value: this.collection[id], value: this.collection[id],
}, },
})); })
);
} }
} }
on_selected(event) { on_selected(event) {
let id = event.srcElement.value; let id = event.srcElement.value;
this.selected_id = id != '' ? id : undefined; this.selected_id = id != '' ? id : undefined;
this.dispatchEvent(new CustomEvent('change', { this.dispatchEvent(
new CustomEvent('change', {
bubbles: true, bubbles: true,
detail: { detail: {
id: id, id: id,
value: this.collection[id], value: this.collection[id],
}, },
})); })
);
} }
render() { render() {
let self = this; let self = this;
return html` return html`
<span style="display: inline-flex; flex-direction: row"> <link rel="stylesheet" href="tildefriends.css"/>
<span class="inline-flex-row">
<select @change=${this.on_selected} id="select" value=${this.selected_id}> <select @change=${this.on_selected} id="select" value=${this.selected_id}>
<option value="" ?selected=${this.selected_id === ''} disabled hidden>(select)</option> <option value="" ?selected=${this.selected_id === ''} disabled hidden>(select ${this.category})</option>
${Object.values(this.collection ?? {}).sort((x, y) => x.name.localeCompare(y.name)).map(x => html`<option value=${x.id} ?selected=${this.selected_id === x.id}>${x.name}</option>`)} ${Object.values(this.collection ?? {})
.sort((x, y) => x.name.localeCompare(y.name))
.map(
(x) =>
html`<option
value=${x.id}
?selected=${this.selected_id === x.id}
>
${x.name}
</option>`
)}
</select> </select>
<span ?hidden=${!this.is_renaming || !this.whoami}> <span ?hidden=${!this.is_renaming || !this.whoami}>
<span style="display: inline-flex; flex-direction: row; margin-left: 8px; margin-right: 8px"> <span class="inline-flex-row" style="margin-left: 8px; margin-right: 8px">
<label for="rename_name">🏷Rename to:</label> <label for="rename_name">🏷Rename to:</label>
<input type="text" id="rename_name"></input> <input type="text" id="rename_name"></input>
<button @click=${this.on_rename}>Rename ${this.type}</button> <button @click=${this.on_rename}>Rename ${this.type}</button>
<button @click=${() => self.is_renaming = false}>x</button> <button @click=${() => (self.is_renaming = false)}>x</button>
</span> </span>
</span> </span>
<button @click=${() => self.is_renaming = true} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button> <button class="yellow" @click=${() => (self.is_renaming = true)} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button>
<button @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button> <button class="red" @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button>
<span ?hidden=${!this.is_creating || !this.whoami}> <span ?hidden=${!this.is_creating || !this.whoami}>
<label for="create_name">New ${this.type} name:</label> <label for="create_name">New ${this.type} name:</label>
<input type="text" id="create_name"></input> <input type="text" id="create_name"></input>
<button @click=${this.on_create}>Create ${this.type}</button> <button @click=${this.on_create}>Create ${this.type}</button>
<button @click=${() => self.is_creating = false}>x</button> <button @click=${() => (self.is_creating = false)}>x</button>
</span> </span>
<button @click=${() => self.is_creating = true} ?hidden=${this.is_creating || !this.whoami}>+</button> <button class="green" @click=${() => (self.is_creating = true)} ?hidden=${this.is_creating || !this.whoami}>+</button>
</span> </span>
`; `;
} }

View File

@ -19,15 +19,23 @@ class TfIdentityPickerElement extends LitElement {
changed(event) { changed(event) {
this.selected = event.srcElement.value; this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', { this.dispatchEvent(
new Event('change', {
srcElement: this, srcElement: this,
})); })
);
} }
render() { render() {
return html` return html`
<link rel="stylesheet" href="tildefriends.css" />
<select @change=${this.changed} style="max-width: 100%"> <select @change=${this.changed} style="max-width: 100%">
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)} ${(this.ids ?? []).map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${id}
</option>`
)}
</select> </select>
`; `;
} }

View File

@ -31,7 +31,7 @@ class TfCollectionsAppElement extends LitElement {
tfrpc.register(function hash_changed(hash) { tfrpc.register(function hash_changed(hash) {
self.notify_hash_changed(hash); self.notify_hash_changed(hash);
}); });
tfrpc.rpc.get_hash().then(hash => self.notify_hash_changed(hash)); tfrpc.rpc.get_hash().then((hash) => self.notify_hash_changed(hash));
} }
async load() { async load() {
@ -49,10 +49,16 @@ class TfCollectionsAppElement extends LitElement {
let max_rowid; let max_rowid;
let wikis; let wikis;
let start_whoami = this.whoami; let start_whoami = this.whoami;
while (true) while (true) {
{
console.log('read_wikis', this.whoami); console.log('read_wikis', this.whoami);
[max_rowid, wikis] = await tfrpc.rpc.collection(this.following, 'wiki', undefined, max_rowid, wikis, false); [max_rowid, wikis] = await tfrpc.rpc.collection(
this.following,
'wiki',
undefined,
max_rowid,
wikis,
false
);
console.log('read ->', wikis); console.log('read ->', wikis);
if (this.whoami !== start_whoami) { if (this.whoami !== start_whoami) {
break; break;
@ -70,9 +76,14 @@ class TfCollectionsAppElement extends LitElement {
let start_id = this.wiki.id; let start_id = this.wiki.id;
let max_rowid; let max_rowid;
let wiki_docs; let wiki_docs;
while (true) while (true) {
{ [max_rowid, wiki_docs] = await tfrpc.rpc.collection(
[max_rowid, wiki_docs] = await tfrpc.rpc.collection(this.wiki?.editors, 'wiki-doc', this.wiki?.id, max_rowid, wiki_docs); this.wiki?.editors,
'wiki-doc',
this.wiki?.id,
max_rowid,
wiki_docs
);
if (this.wiki?.id !== start_id) { if (this.wiki?.id !== start_id) {
break; break;
} }
@ -128,7 +139,11 @@ class TfCollectionsAppElement extends LitElement {
} }
update_hash() { update_hash() {
tfrpc.rpc.set_hash(this.wiki_doc ? `${this.wiki.name}/${this.wiki_doc.name}` : `${this.wiki.name}`); tfrpc.rpc.set_hash(
this.wiki_doc
? `${this.wiki.name}/${this.wiki_doc.name}`
: `${this.wiki.name}`
);
} }
async on_wiki_changed(event) { async on_wiki_changed(event) {
@ -174,7 +189,7 @@ class TfCollectionsAppElement extends LitElement {
if (confirm(`Are you sure you want to remove ${id} as an editor?`)) { if (confirm(`Are you sure you want to remove ${id} as an editor?`)) {
let editors = [...this.wiki.editors]; let editors = [...this.wiki.editors];
if (editors.indexOf(id) != -1) { if (editors.indexOf(id) != -1) {
editors = editors.filter(x => x !== id); editors = editors.filter((x) => x !== id);
} }
await tfrpc.rpc.appendMessage(this.whoami, { await tfrpc.rpc.appendMessage(this.whoami, {
type: 'wiki', type: 'wiki',
@ -240,70 +255,123 @@ class TfCollectionsAppElement extends LitElement {
render() { render() {
let self = this; let self = this;
return html` return html`
<link rel="stylesheet" href="tildefriends.css"/>
<style> <style>
.toc:hover { .toc-item {
white-space: nowrap;
cursor: pointer;
}
.toc-item:hover {
background-color: #0cc; background-color: #0cc;
} }
.toc.selected { .toc-item.selected {
background-color: #088; background-color: #088;
font-weight: bold;
}
.table-of-contents {
flex: 0 0;
margin-right: 16px;
} }
</style> </style>
<div> <div>
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker> <tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
</div> </div>
<div> <div>
${keyed(this.whoami, html`<tf-collection ${keyed(
this.whoami,
html`<tf-collection
.collection=${this.wikis} .collection=${this.wikis}
whoami=${this.whoami} whoami=${this.whoami}
category="wiki"
selected_id=${this.wiki?.id} selected_id=${this.wiki?.id}
@create=${this.on_wiki_create} @create=${this.on_wiki_create}
@rename=${this.on_wiki_rename} @rename=${this.on_wiki_rename}
@tombstone=${this.on_wiki_tombstone} @tombstone=${this.on_wiki_tombstone}
@change=${this.on_wiki_changed}></tf-collection>`)} @change=${this.on_wiki_changed}
${keyed(this.wiki_doc?.id, html`<tf-collection ></tf-collection>`
)}
${keyed(
this.wiki_doc?.id,
html`<tf-collection
.collection=${this.wiki_docs} .collection=${this.wiki_docs}
whoami=${this.whoami} whoami=${this.whoami}
selected_id=${(this.wiki_doc && this.wiki_doc?.parent == this.wiki?.id) ? this.wiki_doc?.id : ''} category="document"
selected_id=${this.wiki_doc &&
this.wiki_doc?.parent == this.wiki?.id
? this.wiki_doc?.id
: ''}
@create=${this.on_wiki_doc_create} @create=${this.on_wiki_doc_create}
@rename=${this.on_wiki_doc_rename} @rename=${this.on_wiki_doc_rename}
@tombstone=${this.on_wiki_doc_tombstone} @tombstone=${this.on_wiki_doc_tombstone}
@change=${this.on_wiki_doc_changed}></tf-collection>`)} @change=${this.on_wiki_doc_changed}
<button @click=${() => self.expand_editors = !self.expand_editors}>${this.wiki?.editors?.length} editor${this.wiki?.editors?.length > 1 ? 's' : ''}</button> ></tf-collection>`
)}
<button @click=${() => (self.expand_editors = !self.expand_editors)}>${this.wiki?.editors?.length} editor${this.wiki?.editors?.length > 1 ? 's' : ''}</button>
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}> <div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
<div> <div>
<ul> <ul>
${this.wiki?.editors.map(id => html`<li><button ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)} ${this.wiki?.editors.map((id) => html`<li><button class="red" ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)}
<li> <li>
<button @click=${() => self.adding_editor = true} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button> <button class="green" @click=${() => (self.adding_editor = true)} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button>
<div ?hidden=${!this.adding_editor}> <div ?hidden=${!this.adding_editor}>
<label for="add_editor">Add Editor:</label> <label for="add_editor">Add Editor:</label>
<input type="text" id="add_editor"></input> <input type="text" id="add_editor"></input>
<button @click=${this.on_add_editor}>Add Editor</button> <button @click=${this.on_add_editor}>Add Editor</button>
<button @click=${() => self.adding_editor = false}>x</button> <button @click=${() => (self.adding_editor = false)}>x</button>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex; flex-direction: row"> <div class="flex-row">
<div style="flex: 0 0"> <div class="box table-of-contents">
${Object.values(this.wikis || {}).sort((x, y) => x.name.localeCompare(y.name)).map(wiki => html` ${Object.values(this.wikis || {})
<div class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_changed({detail: {value: wiki}})}>${wiki.name}</div> .sort((x, y) => x.name.localeCompare(y.name))
<ul> .map(
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html` (wiki) => html`
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc?.private ? '🔒' : '📄'} ${doc.name}</li> <div
`)} class="toc-item ${self.wiki?.id === wiki.id
</ul> ? 'selected'
`)} : ''}"
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
>
${self.wiki?.id === wiki.id ? '' : '>'} ${wiki.name}
</div> </div>
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html` <ul>
${Object.values(self.wiki_docs || {})
.filter((doc) => doc.parent === wiki?.id)
.sort((x, y) => x.name.localeCompare(y.name))
.map(
(doc) => html`
<li
class="toc-item ${self.wiki_doc?.id === doc.id
? 'selected'
: ''}"
style="list-style: none; text-indent: -1rem"
@click=${() =>
self.on_wiki_doc_changed({detail: {value: doc}})}
>
${doc?.private ? '🔒' : '📄'} ${doc.name}
</li>
`
)}
</ul>
`
)}
</div>
${
this.wiki_doc && this.wiki_doc.parent === this.wiki?.id
? html`
<tf-wiki-doc <tf-wiki-doc
style="width: 100%" style="width: 100%"
whoami=${this.whoami} whoami=${this.whoami}
.wiki=${this.wiki} .wiki=${this.wiki}
.value=${this.wiki_doc}></tf-wiki-doc> .value=${this.wiki_doc}
` : undefined} ></tf-wiki-doc>
`
: undefined
}
</div> </div>
`; `;
} }

View File

@ -29,10 +29,16 @@ class TfWikiDocElement extends LitElement {
let node = event.node; let node = event.node;
if (event.entering) { if (event.entering) {
if (node.destination?.startsWith('&')) { if (node.destination?.startsWith('&')) {
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal; node.destination =
'/' +
node.destination +
'/view?filename=' +
node.firstChild?.literal;
} else if (node.type === 'link') { } else if (node.type === 'link') {
if (node.destination.indexOf(':') == -1 && if (
node.destination.indexOf('/') == -1) { node.destination.indexOf(':') == -1 &&
node.destination.indexOf('/') == -1
) {
node.destination = `#${this.wiki?.name}/${node.destination}`; node.destination = `#${this.wiki?.name}/${node.destination}`;
} }
} }
@ -70,13 +76,19 @@ class TfWikiDocElement extends LitElement {
} }
thumbnail(md) { thumbnail(md) {
let m = md ? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/) : undefined; let m = md
? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/)
: undefined;
return m ? m[1] : undefined; return m ? m[1] : undefined;
} }
async load_blob() { async load_blob() {
let blob = await tfrpc.rpc.get_blob(this.value?.blob); let blob = await tfrpc.rpc.get_blob(this.value?.blob);
if (blob.endsWith('.box')) { if (!blob) {
console.warn(
"no blob found, we're going to assume the document is empty (load_blob())"
);
} else if (blob.endsWith('.box')) {
let d = await tfrpc.rpc.try_decrypt(this.whoami, blob); let d = await tfrpc.rpc.try_decrypt(this.whoami, blob);
if (d) { if (d) {
blob = d; blob = d;
@ -106,12 +118,16 @@ class TfWikiDocElement extends LitElement {
key: this.value.id, key: this.value.id,
parent: this.value.parent, parent: this.value.parent,
blob: id, blob: id,
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})), mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
private: this.value?.private, private: this.value?.private,
}; };
if (draft) { if (draft) {
message.recps = this.value.editors; message.recps = this.value.editors;
message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message)); message = await tfrpc.rpc.encrypt(
this.whoami,
this.value.editors,
JSON.stringify(message)
);
} }
await tfrpc.rpc.appendMessage(this.whoami, message); await tfrpc.rpc.appendMessage(this.whoami, message);
this.is_editing = false; this.is_editing = false;
@ -136,7 +152,7 @@ class TfWikiDocElement extends LitElement {
summary: this.summary(blob), summary: this.summary(blob),
thumbnail: this.thumbnail(blob), thumbnail: this.thumbnail(blob),
blog: id, blog: id,
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})), mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
}; };
await tfrpc.rpc.appendMessage(this.whoami, message); await tfrpc.rpc.appendMessage(this.whoami, message);
this.is_editing = false; this.is_editing = false;
@ -155,13 +171,17 @@ class TfWikiDocElement extends LitElement {
let context = canvas.getContext('2d'); let context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height); context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data_url = canvas.toDataURL(mime_type); let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0)); let result = atob(data_url.split(',')[1])
.split('')
.map((x) => x.charCodeAt(0));
resolve(result); resolve(result);
}; };
img.onerror = function (event) { img.onerror = function (event) {
reject(new Error('Failed to load image.')); reject(new Error('Failed to load image.'));
}; };
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join(''); let raw = Array.from(new Uint8Array(buffer))
.map((b) => String.fromCharCode(b))
.join('');
let original = `data:${type};base64,${btoa(raw)}`; let original = `data:${type};base64,${btoa(raw)}`;
img.src = original; img.src = original;
}); });
@ -187,7 +207,11 @@ class TfWikiDocElement extends LitElement {
let best_buffer; let best_buffer;
let best_type; let best_type;
for (let format of ['image/png', 'image/jpeg', 'image/webp']) { for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
let test_buffer = await self.convert_to_format(buffer, file.type, format); let test_buffer = await self.convert_to_format(
buffer,
file.type,
format
);
if (!best_buffer || test_buffer.length < best_buffer.length) { if (!best_buffer || test_buffer.length < best_buffer.length) {
best_buffer = test_buffer; best_buffer = test_buffer;
best_type = format; best_type = format;
@ -233,30 +257,41 @@ class TfWikiDocElement extends LitElement {
let self = this; let self = this;
let thumbnail_ref = this.thumbnail(this.blob); let thumbnail_ref = this.thumbnail(this.blob);
return html` return html`
<link rel="stylesheet" href="tildefriends.css"/>
<style> <style>
a:link { color: #268bd2 } a:link { color: #268bd2 }
a:visited { color: #6c71c4 } a:visited { color: #6c71c4 }
a:hover { color: #859900 } a:hover { color: #859900 }
a:active { color: #2aa198 } a:active { color: #2aa198 }
#editor-text-area {
background-color: #00000040;
color: white;
style="flex: 1 1;
min-height: 10em;
font-size: larger;
${this.value?.private ? 'border: 4px solid #800' : ''}
</style> </style>
<div style="display: inline-flex; flex-direction: row"> <div class="inline-flex-row">
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button> <button ?disabled=${!this.whoami || this.is_editing} @click=${() => (self.is_editing = true)}>Edit</button>
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button> <button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
<button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button> <button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button>
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button> <button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button>
<button ?disabled=${!this.is_editing} @click=${() => self.value = Object.assign({}, self.value, {private: !self.value.private})}>${this.value?.private ? 'Make Public' : 'Make Private'}</button> <button ?disabled=${!this.is_editing} @click=${() => (self.value = Object.assign({}, self.value, {private: !self.value.private}))}>${this.value?.private ? 'Make Public' : 'Make Private'}</button>
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button> <button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button>
</div> </div>
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div> <div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
<div style="display: flex; flex-direction: row; ${this.value?.private ? 'border-top: 4px solid #800' : ''}"> <div class="flex-column" ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
<textarea <textarea
rows="25"
?hidden=${!this.is_editing} ?hidden=${!this.is_editing}
style="flex: 1 1; min-height: 10em; ${this.value?.private ? 'border: 4px solid #800' : ''}" id="editor-text-area"
@input=${this.on_edit} @input=${this.on_edit}
@paste=${this.paste} @paste=${this.paste}
.value=${this.blob ?? ''}></textarea> .value=${this.blob ?? ''}></textarea>
<div style="flex: 1 1"> <div style="flex: 1 1; margin-top: 16px">
<div ?hidden=${!this.is_editing} style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"> <div ?hidden=${!this.is_editing} class="box">
Summary
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view"> <img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1> <h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
${unsafeHTML(this.markdown(this.summary(this.blob)))} ${unsafeHTML(this.markdown(this.summary(this.blob)))}

115
apps/wiki/tildefriends.css Normal file
View File

@ -0,0 +1,115 @@
/*
* Tilde Friends core stylesheet
* This is a prototype; things may change based on feedback.
*
* This Software is an external library that is part of
* Tilde Friends and is shared under the MIT license.
*
* Inject this file in your app at tildefriends.css
* and use this tag to import it:
* <link rel="stylesheet" href="tildefriends.css"/>
*
* Revision 0 / 2024 M02 19
*/
body {
color: white;
font-family: sans-serif;
}
button,
.button,
input[type='button'],
input[type='submit'],
select {
border: none;
border-radius: 8px;
padding: 8px 12px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 4px;
&.red {
background-color: #bd1e24;
color: white;
}
&.green {
background-color: #18922d;
color: white;
}
&.blue {
background-color: #0067a7;
color: white;
}
&.yellow {
background-color: #ee9600;
color: black;
}
&:hover {
filter: brightness(0.75);
}
}
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
table {
border-collapse: collapse;
width: 100%;
}
td,
th {
border: 1px solid #ffffff40;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #ffffff20;
}
.flex {
display: flex;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-row {
display: flex;
flex-direction: row;
}
.inline-flex-row {
display: inline-flex;
flex-direction: row;
}
.box {
background-color: #00000020;
border: 1px solid grey;
border-radius: 8px;
padding: 16px;
margin: 4px;
}

View File

@ -2,7 +2,7 @@ async function process_message(whoami, collection, message, kind, parent) {
let content = JSON.parse(message.content); let content = JSON.parse(message.content);
if (typeof content == 'string') { if (typeof content == 'string') {
let x; let x;
for (let id of (whoami || [])) { for (let id of whoami || []) {
x = await ssb.privateMessageDecrypt(id, content); x = await ssb.privateMessageDecrypt(id, content);
if (x) { if (x) {
try { try {
@ -17,8 +17,7 @@ async function process_message(whoami, collection, message, kind, parent) {
if (!x) { if (!x) {
return; return;
} }
if (content.type !== kind || if (content.type !== kind || (parent && content.parent !== parent)) {
(parent && content.parent !== parent)) {
return; return;
} }
} else { } else {
@ -28,7 +27,10 @@ async function process_message(whoami, collection, message, kind, parent) {
if (content?.tombstone) { if (content?.tombstone) {
delete collection[content.key]; delete collection[content.key];
} else { } else {
collection[content.key] = Object.assign(collection[content.key] || {}, content); collection[content.key] = Object.assign(
collection[content.key] || {},
content
);
} }
} else { } else {
collection[message.id] = Object.assign(content, {id: message.id}); collection[message.id] = Object.assign(content, {id: message.id});
@ -58,26 +60,42 @@ ssb.addEventListener('message', function(id) {
} }
}); });
export async function collection(ids, kind, parent, max_rowid, data, include_private) { export async function collection(
ids,
kind,
parent,
max_rowid,
data,
include_private
) {
let whoami = await ssb.getIdentities(); let whoami = await ssb.getIdentities();
data = data ?? {}; data = data ?? {};
let rowid = 0; let rowid = 0;
let first = true; let first = true;
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid; rowid = row.rowid;
}); }
);
while (true) { while (true) {
if (rowid == max_rowid) { if (rowid == max_rowid) {
await new_message(); await new_message();
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) { await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid; rowid = row.rowid;
}); }
);
first = false; first = false;
} }
let modified = false; let modified = false;
let rows = []; let rows = [];
await ssb.sqlAsync(` await ssb.sqlAsync(
`
SELECT messages.id, author, content, timestamp SELECT messages.id, author, content, timestamp
FROM messages FROM messages
JOIN json_each(?1) AS id ON messages.author = id.value JOIN json_each(?1) AS id ON messages.author = id.value
@ -88,9 +106,19 @@ export async function collection(ids, kind, parent, max_rowid, data, include_pri
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR (?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
(?6 AND content LIKE '"%')) (?6 AND content LIKE '"%'))
ORDER BY timestamp ORDER BY timestamp
`, [JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent, include_private ? true : false], function(row) { `,
[
JSON.stringify(ids),
max_rowid ?? -1,
rowid,
kind,
parent,
include_private ? true : false,
],
function (row) {
rows.push(row); rows.push(row);
}); }
);
max_rowid = rowid; max_rowid = rowid;
for (let row of rows) { for (let row of rows) {
if (await process_message(whoami, data, row, kind, parent)) { if (await process_message(whoami, data, row, kind, parent)) {

View File

@ -30,7 +30,7 @@ function App() {
*/ */
App.prototype.readOutput = function (callback) { App.prototype.readOutput = function (callback) {
this._on_output = callback; this._on_output = callback;
} };
/** /**
* TODOC * TODOC
@ -58,7 +58,7 @@ App.prototype.makeFunction = function(api) {
}; };
Object.defineProperty(result, 'name', {value: api[0], writable: false}); Object.defineProperty(result, 'name', {value: api[0], writable: false});
return result; return result;
} };
/** /**
* TODOC * TODOC
@ -67,7 +67,7 @@ App.prototype.makeFunction = function(api) {
App.prototype.send = function (message) { App.prototype.send = function (message) {
if (this._send_queue) { if (this._send_queue) {
if (this._on_output) { if (this._on_output) {
this._send_queue.forEach(x => this._on_output(x)); this._send_queue.forEach((x) => this._on_output(x));
this._send_queue = null; this._send_queue = null;
} else if (message) { } else if (message) {
this._send_queue.push(message); this._send_queue.push(message);
@ -76,7 +76,7 @@ App.prototype.send = function(message) {
if (message && this._on_output) { if (message && this._on_output) {
this._on_output(message); this._on_output(message);
} }
} };
/** /**
* TODOC * TODOC
@ -97,7 +97,7 @@ function socket(request, response, client) {
if (process) { if (process) {
process.timeout = 0; process.timeout = 0;
} }
} };
response.onMessage = async function (event) { response.onMessage = async function (event) {
if (event.opCode == 0x1 || event.opCode == 0x2) { if (event.opCode == 0x1 || event.opCode == 0x2) {
@ -105,28 +105,33 @@ function socket(request, response, client) {
try { try {
message = JSON.parse(event.data); message = JSON.parse(event.data);
} catch (error) { } catch (error) {
print("ERROR", error, event.data, event.data.length, event.opCode); print('ERROR', error, event.data, event.data.length, event.opCode);
return; return;
} }
if (message.action == "hello") { if (message.action == 'hello') {
let packageOwner; let packageOwner;
let packageName; let packageName;
let blobId; let blobId;
let match; let match;
let parentApp; let parentApp;
if (match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path)) { if (
(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
) {
blobId = match[1]; blobId = match[1];
} else if (match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path)) { } else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
packageOwner = match[1]; packageOwner = match[1];
packageName = match[2]; packageName = match[2];
blobId = await new Database(packageOwner).get('path:' + packageName); blobId = await new Database(packageOwner).get('path:' + packageName);
if (!blobId) { if (!blobId) {
response.send(JSON.stringify({ response.send(
JSON.stringify({
message: 'tfrpc', message: 'tfrpc',
method: "error", method: 'error',
params: [message.path + ' not found'], params: [message.path + ' not found'],
id: -1, id: -1,
}), 0x1); }),
0x1
);
return; return;
} }
if (packageOwner != 'core') { if (packageOwner != 'core') {
@ -137,12 +142,15 @@ function socket(request, response, client) {
}; };
} }
} }
response.send(JSON.stringify({ response.send(
action: "session", JSON.stringify({
action: 'session',
credentials: credentials, credentials: credentials,
parentApp: parentApp, parentApp: parentApp,
id: blobId, id: blobId,
}), 0x1); }),
0x1
);
options.api = message.api || []; options.api = message.api || [];
options.credentials = credentials; options.credentials = credentials;
@ -152,9 +160,16 @@ function socket(request, response, client) {
let sessionId = makeSessionId(); let sessionId = makeSessionId();
if (blobId) { if (blobId) {
if (message.edit_only) { if (message.edit_only) {
response.send(JSON.stringify({action: 'ready', edit_only: true}), 0x1); response.send(
JSON.stringify({action: 'ready', edit_only: true}),
0x1
);
} else { } else {
process = await core.getSessionProcessBlob(blobId, sessionId, options); process = await core.getSessionProcessBlob(
blobId,
sessionId,
options
);
} }
} }
if (process) { if (process) {
@ -177,14 +192,14 @@ function socket(request, response, client) {
again = false; again = false;
} else { } else {
// Idle. Ping them. // Idle. Ping them.
response.send("", 0x9); response.send('', 0x9);
process.lastPing = now; process.lastPing = now;
} }
if (again && process.timeout) { if (again && process.timeout) {
setTimeout(ping, process.timeout); setTimeout(ping, process.timeout);
} }
} };
if (process && process.timeout > 0) { if (process && process.timeout > 0) {
setTimeout(ping, process.timeout); setTimeout(ping, process.timeout);
@ -224,11 +239,16 @@ function socket(request, response, client) {
if (process) { if (process) {
process.lastActive = Date.now(); process.lastActive = Date.now();
} }
} };
response.upgrade(100, refresh ? { response.upgrade(
100,
refresh
? {
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`, 'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
} : {}); }
: {}
);
} }
export {socket, App}; export {socket, App};

View File

@ -1,15 +1,17 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>Tilde Friends Sign-in</title> <title>Tilde Friends Sign-in</title>
<link type="text/css" rel="stylesheet" href="/static/style.css"> <link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png"> <link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
</head> </head>
<body> <body>
<h1 style="text-align: center">Tilde Friends Sign-in</h1> <h1 style="text-align: center">Tilde Friends Sign-in</h1>
<tf-auth id="auth"></tf-auth> <tf-auth id="auth"></tf-auth>
<script>window.litDisableBundleWarning = true;</script> <script>
window.litDisableBundleWarning = true;
</script>
<script type="module"> <script type="module">
import {LitElement, html} from '/lit/lit-all.min.js'; import {LitElement, html} from '/lit/lit-all.min.js';
let g_data = $AUTH_DATA; let g_data = $AUTH_DATA;

View File

@ -1,7 +1,7 @@
import * as core from './core.js'; import * as core from './core.js';
import * as form from './form.js'; import * as form from './form.js';
let gDatabase = new Database("auth"); let gDatabase = new Database('auth');
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000; const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
@ -57,27 +57,17 @@ function makeJwt(payload) {
const final_payload = b64url( const final_payload = b64url(
base64Encode( base64Encode(
JSON.stringify( JSON.stringify(
Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval} Object.assign({}, payload, {
) exp: new Date().valueOf() + kRefreshInterval,
})
) )
) )
); );
const jwt = [ const jwt = [
b64url( b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
base64Encode(
JSON.stringify({
alg: 'HS256',
typ: 'JWT'
})
)
),
final_payload, final_payload,
b64url( b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
ssb.hmacsha256sign(final_payload, ':auth', id)
)
].join('.'); ].join('.');
return jwt; return jwt;
} }
@ -99,7 +89,7 @@ function readSession(session) {
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) { if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload)))); const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
const now = new Date().valueOf() const now = new Date().valueOf();
if (now < result.exp) { if (now < result.exp) {
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`); print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
@ -141,11 +131,15 @@ function hashPassword(password) {
* @returns TODOC * @returns TODOC
*/ */
function noAdministrator() { function noAdministrator() {
return !core.globalSettings || return (
!core.globalSettings ||
!core.globalSettings.permissions || !core.globalSettings.permissions ||
!Object.keys(core.globalSettings.permissions).some(function (name) { !Object.keys(core.globalSettings.permissions).some(function (name) {
return core.globalSettings.permissions[name].indexOf("administration") != -1; return (
}); core.globalSettings.permissions[name].indexOf('administration') != -1
);
})
);
} }
/** /**
@ -159,8 +153,8 @@ function makeAdministrator(name) {
if (!core.globalSettings.permissions[name]) { if (!core.globalSettings.permissions[name]) {
core.globalSettings.permissions[name] = []; core.globalSettings.permissions[name] = [];
} }
if (core.globalSettings.permissions[name].indexOf("administration") == -1) { if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
core.globalSettings.permissions[name].push("administration"); core.globalSettings.permissions[name].push('administration');
} }
core.setGlobalSettings(core.globalSettings); core.setGlobalSettings(core.globalSettings);
@ -177,7 +171,7 @@ function getCookies(headers) {
if (headers.cookie) { if (headers.cookie) {
let parts = headers.cookie.split(/,|;/); let parts = headers.cookie.split(/,|;/);
for (let i in parts) { for (let i in parts) {
let equals = parts[i].indexOf("="); let equals = parts[i].indexOf('=');
let name = parts[i].substring(0, equals).trim(); let name = parts[i].substring(0, equals).trim();
let value = parts[i].substring(equals + 1).trim(); let value = parts[i].substring(equals + 1).trim();
cookies[name] = value; cookies[name] = value;
@ -195,7 +189,17 @@ function getCookies(headers) {
function isNameValid(name) { function isNameValid(name) {
// TODO(tasiaiso): convert this into a regex // TODO(tasiaiso): convert this into a regex
let c = name.charAt(0); let c = name.charAt(0);
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9')); return (
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
name
.split()
.map(
(x) =>
x >= ('a' && x <= 'z') ||
x >= ('A' && x <= 'Z') ||
x >= ('0' && x <= '9')
)
);
} }
/** /**
@ -207,14 +211,19 @@ function isNameValid(name) {
function handler(request, response) { function handler(request, response) {
// TODO(tasiaiso): split this function // TODO(tasiaiso): split this function
let session = getCookies(request.headers).session; let session = getCookies(request.headers).session;
if (request.uri == '/login') {
if (request.uri == "/login") {
let formData = form.decodeForm(request.query); let formData = form.decodeForm(request.query);
if (query(request.headers)?.permissions?.authenticated) { if (query(request.headers)?.permissions?.authenticated) {
if (formData.return) { if (formData.return) {
response.writeHead(303, {"Location": formData.return}); response.writeHead(303, {Location: formData.return});
} else { } else {
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"}); response.writeHead(303, {
Location:
(request.client.tls ? 'https://' : 'http://') +
request.headers.host +
'/',
'Content-Length': '0',
});
} }
response.end(); response.end();
return; return;
@ -223,22 +232,23 @@ function handler(request, response) {
let sessionIsNew = false; let sessionIsNew = false;
let loginError; let loginError;
if (request.method == "POST" || formData.submit) { if (request.method == 'POST' || formData.submit) {
sessionIsNew = true; sessionIsNew = true;
formData = form.decodeForm(utf8Decode(request.body), formData); formData = form.decodeForm(utf8Decode(request.body), formData);
if (formData.submit == "Login") { if (formData.submit == 'Login') {
let account = gDatabase.get("user:" + formData.name); let account = gDatabase.get('user:' + formData.name);
account = account ? JSON.parse(account) : account; account = account ? JSON.parse(account) : account;
if (formData.register == '1') { if (formData.register == '1') {
if (!account && if (
!account &&
isNameValid(formData.name) && isNameValid(formData.name) &&
formData.password == formData.confirm) { formData.password == formData.confirm
) {
let users = new Set(); let users = new Set();
let users_original = gDatabase.get('users'); let users_original = gDatabase.get('users');
try { try {
users = new Set(JSON.parse(users_original)); users = new Set(JSON.parse(users_original));
} catch { } catch {}
}
if (!users.has(formData.name)) { if (!users.has(formData.name)) {
users.add(formData.name); users.add(formData.name);
} }
@ -256,10 +266,12 @@ function handler(request, response) {
loginError = 'Error registering account.'; loginError = 'Error registering account.';
} }
} else if (formData.change == '1') { } else if (formData.change == '1') {
if (account && if (
account &&
isNameValid(formData.name) && isNameValid(formData.name) &&
formData.new_password == formData.confirm && formData.new_password == formData.confirm &&
verifyPassword(formData.password, account.password)) { verifyPassword(formData.password, account.password)
) {
session = makeJwt({name: formData.name}); session = makeJwt({name: formData.name});
account = {password: hashPassword(formData.new_password)}; account = {password: hashPassword(formData.new_password)};
gDatabase.set('user:' + formData.name, JSON.stringify(account)); gDatabase.set('user:' + formData.name, JSON.stringify(account));
@ -267,9 +279,11 @@ function handler(request, response) {
loginError = 'Error changing password.'; loginError = 'Error changing password.';
} }
} else { } else {
if (account && if (
account &&
account.password && account.password &&
verifyPassword(formData.password, account.password)) { verifyPassword(formData.password, account.password)
) {
session = makeJwt({name: formData.name}); session = makeJwt({name: formData.name});
if (noAdministrator()) { if (noAdministrator()) {
makeAdministrator(formData.name); makeAdministrator(formData.name);
@ -287,10 +301,14 @@ function handler(request, response) {
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`; let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
let entry = readSession(session); let entry = readSession(session);
if (entry && formData.return) { if (entry && formData.return) {
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie}); response.writeHead(303, {
Location: formData.return,
'Set-Cookie': cookie,
});
response.end(); response.end();
} else { } else {
File.readFile("core/auth.html").then(function(data) { File.readFile('core/auth.html')
.then(function (data) {
let html = utf8Decode(data); let html = utf8Decode(data);
let auth_data = { let auth_data = {
session_is_new: sessionIsNew, session_is_new: sessionIsNew,
@ -299,20 +317,36 @@ function handler(request, response) {
code_of_conduct: core.globalSettings.code_of_conduct, code_of_conduct: core.globalSettings.code_of_conduct,
have_administrator: !noAdministrator(), have_administrator: !noAdministrator(),
}; };
html = utf8Encode(html.replace('$AUTH_DATA', JSON.stringify(auth_data))); html = utf8Encode(
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": html.length}); html.replace('$AUTH_DATA', JSON.stringify(auth_data))
);
response.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Set-Cookie': cookie,
'Content-Length': html.length,
});
response.end(html); response.end(html);
}).catch(function(error) { })
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); .catch(function (error) {
response.end("404 File not found"); response.writeHead(404, {
'Content-Type': 'text/plain; charset=utf-8',
Connection: 'close',
});
response.end('404 File not found');
}); });
} }
} else if (request.uri == "/login/logout") { } else if (request.uri == '/login/logout') {
response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`, "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; HttpOnly`,
Location: '/login' + (request.query ? '?' + request.query : ''),
});
response.end(); response.end();
} else { } else {
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"}); response.writeHead(200, {
response.end("Hello, " + request.client.peerName + "."); 'Content-Type': 'text/plain; charset=utf-8',
Connection: 'close',
});
response.end('Hello, ' + request.client.peerName + '.');
} }
} }
@ -326,7 +360,7 @@ function getPermissions(session) {
let entry = readSession(session); let entry = readSession(session);
if (entry) { if (entry) {
permissions = getPermissionsForUser(entry.name); permissions = getPermissionsForUser(entry.name);
permissions.authenticated = entry.name !== "guest"; permissions.authenticated = entry.name !== 'guest';
} }
return permissions || {}; return permissions || {};
} }
@ -338,7 +372,11 @@ function getPermissions(session) {
*/ */
function getPermissionsForUser(userName) { function getPermissionsForUser(userName) {
let permissions = {}; let permissions = {};
if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) { if (
core.globalSettings &&
core.globalSettings.permissions &&
core.globalSettings.permissions[userName]
) {
for (let i in core.globalSettings.permissions[userName]) { for (let i in core.globalSettings.permissions[userName]) {
permissions[core.globalSettings.permissions[userName][i]] = true; permissions[core.globalSettings.permissions[userName][i]] = true;
} }
@ -355,10 +393,12 @@ function query(headers) {
let session = getCookies(headers).session; let session = getCookies(headers).session;
let entry; let entry;
let autologin = tildefriends.args.autologin; let autologin = tildefriends.args.autologin;
if (entry = autologin ? {name: autologin} : readSession(session)) { if ((entry = autologin ? {name: autologin} : readSession(session))) {
return { return {
session: entry, session: entry,
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session), permissions: autologin
? getPermissionsForUser(autologin)
: getPermissions(session),
}; };
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,12 @@
* @returns * @returns
*/ */
function decode(encoded) { function decode(encoded) {
let result = ""; let result = '';
for (let i = 0; i < encoded.length; i++) { for (let i = 0; i < encoded.length; i++) {
let c = encoded[i]; let c = encoded[i];
if (c == "+") { if (c == '+') {
result += " "; result += ' ';
} else if (c == "%") { } else if (c == '%') {
result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16)); result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
i += 2; i += 2;
} else { } else {

View File

@ -6,12 +6,12 @@
*/ */
function parseUrl(url) { function parseUrl(url) {
// XXX: Hack. // XXX: Hack.
let match = url.match(new RegExp("(\\w+)://([^/:]+)(?::(\\d+))?(.*)")); let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
return { return {
protocol: match[1], protocol: match[1],
host: match[2], host: match[2],
path: match[4], path: match[4],
port: match[3] ? parseInt(match[3]) : match[1] == "http" ? 80 : 443, port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
}; };
} }
@ -32,7 +32,7 @@ function parseResponse(data) {
} else if (!firstLine) { } else if (!firstLine) {
firstLine = line; firstLine = line;
} else { } else {
let colon = line.indexOf(":"); let colon = line.indexOf(':');
headers[line.substring(colon)] = line.substring(colon + 1); headers[line.substring(colon)] = line.substring(colon + 1);
} }
} }
@ -55,7 +55,9 @@ export function fetch(url, options, allowed_hosts) {
let socket = new Socket(); let socket = new Socket();
let buffer = new Uint8Array(0); let buffer = new Uint8Array(0);
return socket.connect(parsed.host, parsed.port).then(function() { return socket
.connect(parsed.host, parsed.port)
.then(function () {
socket.read(function (data) { socket.read(function (data) {
if (data && data.length) { if (data && data.length) {
let newBuffer = new Uint8Array(buffer.length + data.length); let newBuffer = new Uint8Array(buffer.length + data.length);
@ -90,14 +92,21 @@ export function fetch(url, options, allowed_hosts) {
if (parsed.port == 443) { if (parsed.port == 443) {
return socket.startTls(); return socket.startTls();
} }
}).then(function() { })
let body = typeof options?.body == 'string' ? utf8Encode(options.body) : (options.body || new Uint8Array(0)); .then(function () {
let headers = utf8Encode(`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`); let body =
typeof options?.body == 'string'
? utf8Encode(options.body)
: options.body || new Uint8Array(0);
let headers = utf8Encode(
`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
);
let fullRequest = new Uint8Array(headers.length + body.length); let fullRequest = new Uint8Array(headers.length + body.length);
fullRequest.set(headers, 0); fullRequest.set(headers, 0);
fullRequest.set(body, headers.length); fullRequest.set(body, headers.length);
socket.write(fullRequest); socket.write(fullRequest);
}).catch(function(error) { })
.catch(function (error) {
reject(error); reject(error);
}); });
}); });

View File

@ -1,46 +1,156 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>Tilde Friends</title> <title>Tilde Friends</title>
<link type="text/css" rel="stylesheet" href="/static/style.css"> <link type="text/css" rel="stylesheet" href="/static/style.css" />
<link type="text/css" rel="stylesheet" href="/static/w3.css"> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
<link type="image/png" rel="shortcut icon" href="/static/favicon.png"> <link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script> <script>
function set_access_key_title(event) { function set_access_key_title(event) {
if (!event.srcElement.title) { if (!event.srcElement.title) {
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || ('⌨️' + event.srcElement.accessKey)).toUpperCase()}]`; event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || '⌨️' + event.srcElement.accessKey).toUpperCase()}]`;
} }
} }
</script> </script>
</head> </head>
<body style="display: flex; flex-flow: column; width: 100vw; height: 100vh; position: absolute; max-width: 100%; max-height: 100%"> <body
style="
display: flex;
flex-flow: column;
width: 100vw;
height: 100vh;
position: absolute;
max-width: 100%;
max-height: 100%;
"
>
<tf-navigation></tf-navigation> <tf-navigation></tf-navigation>
<div id="content" class="hbox" style="flex: 1 0; overflow: auto"> <div id="content" class="hbox" style="flex: 1 0; overflow: auto">
<div id="editPane" class="vbox" style="flex: 0 1 100%; display: none; overflow: auto"> <div
id="editPane"
class="vbox"
style="flex: 0 1 100%; display: none; overflow: auto"
>
<div class="navigation w3-bar" style="display: flex"> <div class="navigation w3-bar" style="display: flex">
<button class="w3-bar-item w3-button w3-blue" id="closeEditor" name="closeEditor" accesskey="c" onmouseover="set_access_key_title(event)" data-tip="Close the editor">Close</button> <button
<button class="w3-bar-item w3-button w3-blue" id="save" name="save" accesskey="s" onmouseover="set_access_key_title(event)" data-tip="Save the app under the given path">Save</button> class="w3-bar-item w3-button w3-blue"
<button class="w3-bar-item w3-button w3-blue" id="icon" name="icon" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Set an icon/emoji for the app">📦</button> id="closeEditor"
<button class="w3-bar-item w3-button w3-blue" id="export" name="export" accesskey="e" onmouseover="set_access_key_title(event)" data-tip="Export app to .zip file">Export</button> name="closeEditor"
<button class="w3-bar-item w3-button w3-blue" id="import" name="import" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Import app from .zip file">Import</button> accesskey="c"
<button class="w3-bar-item w3-button w3-blue" id="pretty" name="pretty" accesskey="p" onmouseover="set_access_key_title(event)" data-tip="Clean up source formatting">🧼</button> onmouseover="set_access_key_title(event)"
<input class="w3-bar-item w3-input w3-border w3-blue" type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input> data-tip="Close the editor"
<button class="w3-bar-item w3-button w3-blue" id="delete" name="delete" accesskey="d" onmouseover="set_access_key_title(event)" data-tip="Delete the app">Delete</button> >
<button class="w3-bar-item w3-button w3-blue" id="trace_button" accesskey="t" onmouseover="set_access_key_title(event)" data-tip="Open a performance trace for the server">Trace</button> Close
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="save"
name="save"
accesskey="s"
onmouseover="set_access_key_title(event)"
data-tip="Save the app under the given path"
>
Save
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="icon"
name="icon"
accesskey="j"
onmouseover="set_access_key_title(event)"
data-tip="Set an icon/emoji for the app"
>
📦
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="export"
name="export"
accesskey="e"
onmouseover="set_access_key_title(event)"
data-tip="Export app to .zip file"
>
Export
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="import"
name="import"
accesskey="i"
onmouseover="set_access_key_title(event)"
data-tip="Import app from .zip file"
>
Import
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="pretty"
name="pretty"
accesskey="p"
onmouseover="set_access_key_title(event)"
data-tip="Clean up source formatting"
>
🧼
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="whitespace"
name="whitespace"
accesskey="w"
onmouseover="set_access_key_title(event)"
data-tip="Toggle visible whitespace"
>
</button>
<input
class="w3-bar-item w3-input w3-border w3-blue"
type="text"
id="name"
name="name"
style="flex: 1 1; min-width: 1em"
/>
<button
class="w3-bar-item w3-button w3-blue"
id="delete"
name="delete"
accesskey="d"
onmouseover="set_access_key_title(event)"
data-tip="Delete the app"
>
Delete
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="trace_button"
accesskey="t"
onmouseover="set_access_key_title(event)"
data-tip="Open a performance trace for the server"
>
Trace
</button>
</div> </div>
<div class="hbox" style="flex: 1 1; overflow: auto"> <div class="hbox" style="flex: 1 1; overflow: auto">
<div style="overflow: auto"> <div style="overflow: auto">
<tf-files-pane style="overflow: auto"></tf-files-pane> <tf-files-pane style="overflow: auto"></tf-files-pane>
</div> </div>
<div style="flex: 1 1; overflow: auto"><div id="editor" style="width: 100%; height: 100%"></div></div> <div style="flex: 1 1; overflow: auto">
<style id="editor_style"></style>
<div id="editor" style="width: 100%; height: 100%"></div>
</div>
</div> </div>
</div> </div>
<div id="viewPane" class="vbox" style="flex: 0 1 100%; overflow: auto"> <div id="viewPane" class="vbox" style="flex: 0 1 100%; overflow: auto">
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe> <iframe
id="document"
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
style="width: 100%; height: 100%; border: 0"
></iframe>
</div> </div>
</div> </div>
<script>window.litDisableBundleWarning = true;</script> <script>
window.litDisableBundleWarning = true;
</script>
<script src="/static/client.js" type="module"></script> <script src="/static/client.js" type="module"></script>
</body> </body>
</html> </html>

View File

@ -102,22 +102,54 @@ a:active {
} }
/* Solarized Color Scheme Colors */ /* Solarized Color Scheme Colors */
.base03 { color: #002b36; } .base03 {
.base02 { color: #073642; } color: #002b36;
.base01 { color: #586e75; } }
.base00 { color: #657b83; } .base02 {
.base0 { color: #839496; } color: #073642;
.base1 { color: #93a1a1; } }
.base2 { color: #eee8d5; } .base01 {
.base3 { color: #fdf6e3; } color: #586e75;
.yellow { color: #b58900; } }
.orange { color: #cb4b16; } .base00 {
.red { color: #dc322f; } color: #657b83;
.magenta { color: #d33682; } }
.violet { color: #6c71c4; } .base0 {
.blue { color: #268bd2; } color: #839496;
.cyan { color: #2aa198; } }
.green { color: #859900; } .base1 {
color: #93a1a1;
}
.base2 {
color: #eee8d5;
}
.base3 {
color: #fdf6e3;
}
.yellow {
color: #b58900;
}
.orange {
color: #cb4b16;
}
.red {
color: #dc322f;
}
.magenta {
color: #d33682;
}
.violet {
color: #6c71c4;
}
.blue {
color: #268bd2;
}
.cyan {
color: #2aa198;
}
.green {
color: #859900;
}
.permissions { .permissions {
position: absolute; position: absolute;

View File

@ -8,7 +8,11 @@ let g_calls = {};
* @returns * @returns
*/ */
function get_is_browser() { function get_is_browser() {
try { return window !== undefined && console !== undefined; } catch { return false; } try {
return window !== undefined && console !== undefined;
} catch {
return false;
}
} }
if (k_is_browser) { if (k_is_browser) {
@ -32,12 +36,22 @@ function make_rpc(target, prop, receiver) {
g_calls[id] = {resolve: resolve, reject: reject}; g_calls[id] = {resolve: resolve, reject: reject};
}); });
if (k_is_browser) { if (k_is_browser) {
window.parent.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}, '*'); window.parent.postMessage(
{message: 'tfrpc', method: prop, params: [...arguments], id: id},
'*'
);
return promise; return promise;
} else { } else {
return app.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}).then(x => promise); return app
} .postMessage({
message: 'tfrpc',
method: prop,
params: [...arguments],
id: id,
})
.then((x) => promise);
} }
};
} }
/** /**
@ -63,16 +77,22 @@ function call_rpc(message) {
let method = g_api[message.method]; let method = g_api[message.method];
if (method) { if (method) {
try { try {
Promise.resolve(method(...message.params)).then(function(result) { Promise.resolve(method(...message.params))
.then(function (result) {
send({message: 'tfrpc', id: id, result: result}); send({message: 'tfrpc', id: id, result: result});
}).catch(function(error) { })
.catch(function (error) {
send({message: 'tfrpc', id: id, error: error}); send({message: 'tfrpc', id: id, error: error});
}); });
} catch (error) { } catch (error) {
send({message: 'tfrpc', id: id, error: error}); send({message: 'tfrpc', id: id, error: error});
} }
} else { } else {
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`}); send({
message: 'tfrpc',
id: id,
error: `Method '${message.method}' not found.`,
});
} }
} else if (message.error !== undefined) { } else if (message.error !== undefined) {
if (g_calls[id]) { if (g_calls[id]) {

File diff suppressed because one or more lines are too long

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

@ -7,21 +7,21 @@
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.2.1", "@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.8", "@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"rollup": "^4.9.6" "rollup": "^4.13.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-terser": "^0.4.4" "@rollup/plugin-terser": "^0.4.4"
} }
}, },
"node_modules/@codemirror/autocomplete": { "node_modules/@codemirror/autocomplete": {
"version": "6.12.0", "version": "6.15.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.15.0.tgz",
"integrity": "sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==", "integrity": "sha512-G2Zm0mXznxz97JhaaOdoEG2cVupn4JjPaS4AcNvZzhOsnnG9YVN68VzfoUw6dYTsIxT6a/cmoFEN47KAWhXaOg==",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -75,9 +75,9 @@
} }
}, },
"node_modules/@codemirror/lang-javascript": { "node_modules/@codemirror/lang-javascript": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
"integrity": "sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==", "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
@ -88,24 +88,6 @@
"@lezer/javascript": "^1.0.0" "@lezer/javascript": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-javascript/node_modules/@lezer/javascript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.13.tgz",
"integrity": "sha512-5IBr8LIO3xJdJH1e9aj/ZNLE4LSbdsx25wFmGRAZsj2zSmwAYjx26JyU/BYOCpRQlu1jcv1z3vy4NB9+UkfRow==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@codemirror/lang-javascript/node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/lang-json": { "node_modules/@codemirror/lang-json": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
@ -128,19 +110,6 @@
"style-mod": "^4.0.0" "style-mod": "^4.0.0"
} }
}, },
"node_modules/@codemirror/language/node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/language/node_modules/style-mod": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz",
"integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA=="
},
"node_modules/@codemirror/lint": { "node_modules/@codemirror/lint": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz",
@ -151,11 +120,6 @@
"crelt": "^1.0.5" "crelt": "^1.0.5"
} }
}, },
"node_modules/@codemirror/lint/node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/@codemirror/search": { "node_modules/@codemirror/search": {
"version": "6.5.6", "version": "6.5.6",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
@ -166,11 +130,6 @@
"crelt": "^1.0.5" "crelt": "^1.0.5"
} }
}, },
"node_modules/@codemirror/search/node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/@codemirror/state": { "node_modules/@codemirror/state": {
"version": "6.4.1", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
@ -188,24 +147,72 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.24.0", "version": "6.25.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.25.1.tgz",
"integrity": "sha512-zK6m5pNkdhdJl8idPP1gA4N8JKTiSsOz8U/Iw+C1ChMwyLG7+MLiNXnH/wFuAk6KeGEe33/adOiAh5jMqee03w==", "integrity": "sha512-2LXLxsQnHDdfGzDvjzAwZh2ZviNJm7im6tGpa0IONIDnFd8RZ80D2SNi8PDi6YjKcMoMRK20v6OmKIdsrwsyoQ==",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
"style-mod": "^4.1.0", "style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4" "w3c-keyname": "^2.2.4"
} }
}, },
"node_modules/@codemirror/view/node_modules/style-mod": { "node_modules/@jridgewell/gen-mapping": {
"version": "4.1.0", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==" "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
}, },
"node_modules/@codemirror/view/node_modules/w3c-keyname": { "engines": {
"version": "2.2.8", "node": ">=6.0.0"
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", }
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
}, },
"node_modules/@lezer/common": { "node_modules/@lezer/common": {
"version": "1.2.1", "version": "1.2.1",
@ -222,14 +229,6 @@
"@lezer/lr": "^1.0.0" "@lezer/lr": "^1.0.0"
} }
}, },
"node_modules/@lezer/css/node_modules/@lezer/lr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/highlight": { "node_modules/@lezer/highlight": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
@ -239,21 +238,23 @@
} }
}, },
"node_modules/@lezer/html": { "node_modules/@lezer/html": {
"version": "1.3.8", "version": "1.3.9",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.8.tgz", "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.9.tgz",
"integrity": "sha512-EXseJ3pUzWxE6XQBQdqWHZqqlGQRSuNMBcLb6mZWS2J2v+QZhOObD+3ZIKIcm59ntTzyor4LqFTb72iJc3k23Q==", "integrity": "sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0" "@lezer/lr": "^1.0.0"
} }
}, },
"node_modules/@lezer/html/node_modules/@lezer/lr": { "node_modules/@lezer/javascript": {
"version": "1.4.0", "version": "1.4.13",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.13.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", "integrity": "sha512-5IBr8LIO3xJdJH1e9aj/ZNLE4LSbdsx25wFmGRAZsj2zSmwAYjx26JyU/BYOCpRQlu1jcv1z3vy4NB9+UkfRow==",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
} }
}, },
"node_modules/@lezer/json": { "node_modules/@lezer/json": {
@ -266,7 +267,7 @@
"@lezer/lr": "^1.0.0" "@lezer/lr": "^1.0.0"
} }
}, },
"node_modules/@lezer/json/node_modules/@lezer/lr": { "node_modules/@lezer/lr": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
"integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==", "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
@ -341,31 +342,10 @@
} }
} }
}, },
"node_modules/@rollup/pluginutils/node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@rollup/pluginutils/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/@rollup/pluginutils/node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
"integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -375,9 +355,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
"integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -387,9 +367,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
"integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -399,9 +379,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
"integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -411,9 +391,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
"integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -423,9 +403,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
"integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -435,9 +415,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
"integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -447,9 +427,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
"integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -458,10 +438,34 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
"integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
"integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
"integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -471,9 +475,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
"integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -483,9 +487,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
"integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -494,11 +498,45 @@
"win32" "win32"
] ]
}, },
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
}, },
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/codemirror": { "node_modules/codemirror": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
@ -513,6 +551,17 @@
"@codemirror/view": "^6.0.0" "@codemirror/view": "^6.0.0"
} }
}, },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@ -521,6 +570,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -534,6 +588,25 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-builtin-module": { "node_modules/is-builtin-module": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
@ -548,15 +621,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-builtin-module/node_modules/builtin-modules": { "node_modules/is-core-module": {
"version": "3.3.0", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"engines": { "dependencies": {
"node": ">=6" "hasown": "^2.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-module": { "node_modules/is-module": {
@ -564,6 +637,31 @@
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
}, },
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -580,56 +678,10 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resolve/node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve/node_modules/hasown": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/resolve/node_modules/is-core-module": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dependencies": {
"hasown": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve/node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/resolve/node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.12.0", "version": "4.13.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
"integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
}, },
@ -641,70 +693,23 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.12.0", "@rollup/rollup-android-arm-eabi": "4.13.0",
"@rollup/rollup-android-arm64": "4.12.0", "@rollup/rollup-android-arm64": "4.13.0",
"@rollup/rollup-darwin-arm64": "4.12.0", "@rollup/rollup-darwin-arm64": "4.13.0",
"@rollup/rollup-darwin-x64": "4.12.0", "@rollup/rollup-darwin-x64": "4.13.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.12.0", "@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
"@rollup/rollup-linux-arm64-gnu": "4.12.0", "@rollup/rollup-linux-arm64-gnu": "4.13.0",
"@rollup/rollup-linux-arm64-musl": "4.12.0", "@rollup/rollup-linux-arm64-musl": "4.13.0",
"@rollup/rollup-linux-riscv64-gnu": "4.12.0", "@rollup/rollup-linux-riscv64-gnu": "4.13.0",
"@rollup/rollup-linux-x64-gnu": "4.12.0", "@rollup/rollup-linux-x64-gnu": "4.13.0",
"@rollup/rollup-linux-x64-musl": "4.12.0", "@rollup/rollup-linux-x64-musl": "4.13.0",
"@rollup/rollup-win32-arm64-msvc": "4.12.0", "@rollup/rollup-win32-arm64-msvc": "4.13.0",
"@rollup/rollup-win32-ia32-msvc": "4.12.0", "@rollup/rollup-win32-ia32-msvc": "4.13.0",
"@rollup/rollup-win32-x64-msvc": "4.12.0", "@rollup/rollup-win32-x64-msvc": "4.13.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/safe-buffer": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
"integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz",
"integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/rollup/node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
}
},
"node_modules/serialize-javascript/node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/serialize-javascript/node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
@ -724,16 +729,60 @@
} }
] ]
}, },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"dependencies": {
"randombytes": "^2.1.0"
}
},
"node_modules/smob": { "node_modules/smob": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.4.1.tgz",
"integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==", "integrity": "sha512-9LK+E7Hv5R9u4g4C3p+jjLstaLe11MDsL21UpYaCNmapvMkYhqCV4A/f/3gyH8QjMyh6l68q9xC85vihY9ahMQ==",
"dev": true "dev": true
}, },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.27.2", "version": "5.29.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.2.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz",
"integrity": "sha512-sHXmLSkImesJ4p5apTeT63DsV4Obe1s37qT8qvwHRmVxKTBH7Rv9Wr26VcAMmLbmk9UliiwK8z+657NyJHHy/w==", "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
@ -748,106 +797,10 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/terser/node_modules/@jridgewell/gen-mapping": { "node_modules/w3c-keyname": {
"version": "0.3.3", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/terser/node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/terser/node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/terser/node_modules/@jridgewell/source-map": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"node_modules/terser/node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"node_modules/terser/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
"integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/terser/node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/terser/node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/terser/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/terser/node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
} }
} }
} }

View File

@ -5,12 +5,12 @@
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.2.1", "@codemirror/lang-css": "^6.2.1",
"@codemirror/lang-html": "^6.4.8", "@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"rollup": "^4.9.6" "rollup": "^4.13.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-terser": "^0.4.4" "@rollup/plugin-terser": "^0.4.4"

View File

@ -1,16 +0,0 @@
/*
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* This file is only used by HP C on VMS, and is included automatically
* after each header file from this directory
*/
/* restore state. Must correspond to the save in __decc_include_prologue.h */
#pragma names restore

View File

@ -1,20 +0,0 @@
/*
* Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* This file is only used by HP C on VMS, and is included automatically
* after each header file from this directory
*/
/* save state */
#pragma names save
/* have the compiler shorten symbols larger than 31 chars to 23 chars
* followed by a 8 hex char CRC
*/
#pragma names as_is,shortened

View File

@ -1,514 +0,0 @@
/*
* Copyright 2019-2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#ifndef OSSL_AES_PLATFORM_H
# define OSSL_AES_PLATFORM_H
# pragma once
# include <openssl/aes.h>
# ifdef VPAES_ASM
int vpaes_set_encrypt_key(const unsigned char *userKey, int bits,
AES_KEY *key);
int vpaes_set_decrypt_key(const unsigned char *userKey, int bits,
AES_KEY *key);
void vpaes_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void vpaes_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void vpaes_cbc_encrypt(const unsigned char *in,
unsigned char *out,
size_t length,
const AES_KEY *key, unsigned char *ivec, int enc);
# endif /* VPAES_ASM */
# ifdef BSAES_ASM
void ossl_bsaes_cbc_encrypt(const unsigned char *in, unsigned char *out,
size_t length, const AES_KEY *key,
unsigned char ivec[16], int enc);
void ossl_bsaes_ctr32_encrypt_blocks(const unsigned char *in,
unsigned char *out, size_t len,
const AES_KEY *key,
const unsigned char ivec[16]);
void ossl_bsaes_xts_encrypt(const unsigned char *inp, unsigned char *out,
size_t len, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char iv[16]);
void ossl_bsaes_xts_decrypt(const unsigned char *inp, unsigned char *out,
size_t len, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char iv[16]);
# endif /* BSAES_ASM */
# ifdef AES_CTR_ASM
void AES_ctr32_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key,
const unsigned char ivec[AES_BLOCK_SIZE]);
# endif /* AES_CTR_ASM */
# ifdef AES_XTS_ASM
void AES_xts_encrypt(const unsigned char *inp, unsigned char *out, size_t len,
const AES_KEY *key1, const AES_KEY *key2,
const unsigned char iv[16]);
void AES_xts_decrypt(const unsigned char *inp, unsigned char *out, size_t len,
const AES_KEY *key1, const AES_KEY *key2,
const unsigned char iv[16]);
# endif /* AES_XTS_ASM */
# if defined(OPENSSL_CPUID_OBJ)
# if (defined(__powerpc__) || defined(__ppc__) || defined(_ARCH_PPC))
# include "crypto/ppc_arch.h"
# ifdef VPAES_ASM
# define VPAES_CAPABLE (OPENSSL_ppccap_P & PPC_ALTIVEC)
# endif
# if !defined(OPENSSL_SYS_AIX) && !defined(OPENSSL_SYS_MACOSX)
# define HWAES_CAPABLE (OPENSSL_ppccap_P & PPC_CRYPTO207)
# define HWAES_set_encrypt_key aes_p8_set_encrypt_key
# define HWAES_set_decrypt_key aes_p8_set_decrypt_key
# define HWAES_encrypt aes_p8_encrypt
# define HWAES_decrypt aes_p8_decrypt
# define HWAES_cbc_encrypt aes_p8_cbc_encrypt
# define HWAES_ctr32_encrypt_blocks aes_p8_ctr32_encrypt_blocks
# define HWAES_xts_encrypt aes_p8_xts_encrypt
# define HWAES_xts_decrypt aes_p8_xts_decrypt
# define PPC_AES_GCM_CAPABLE (OPENSSL_ppccap_P & PPC_MADD300)
# define AES_GCM_ENC_BYTES 128
# define AES_GCM_DEC_BYTES 128
size_t ppc_aes_gcm_encrypt(const unsigned char *in, unsigned char *out,
size_t len, const void *key, unsigned char ivec[16],
u64 *Xi);
size_t ppc_aes_gcm_decrypt(const unsigned char *in, unsigned char *out,
size_t len, const void *key, unsigned char ivec[16],
u64 *Xi);
# define AES_GCM_ASM_PPC(gctx) ((gctx)->ctr==aes_p8_ctr32_encrypt_blocks && \
(gctx)->gcm.funcs.ghash==gcm_ghash_p8)
void gcm_ghash_p8(u64 Xi[2],const u128 Htable[16],const u8 *inp, size_t len);
# endif /* OPENSSL_SYS_AIX || OPENSSL_SYS_MACOSX */
# endif /* PPC */
# if (defined(__arm__) || defined(__arm) || defined(__aarch64__) || defined(_M_ARM64))
# include "arm_arch.h"
# if __ARM_MAX_ARCH__>=7
# if defined(BSAES_ASM)
# define BSAES_CAPABLE (OPENSSL_armcap_P & ARMV7_NEON)
# endif
# if defined(VPAES_ASM)
# define VPAES_CAPABLE (OPENSSL_armcap_P & ARMV7_NEON)
# endif
# define HWAES_CAPABLE (OPENSSL_armcap_P & ARMV8_AES)
# define HWAES_set_encrypt_key aes_v8_set_encrypt_key
# define HWAES_set_decrypt_key aes_v8_set_decrypt_key
# define HWAES_encrypt aes_v8_encrypt
# define HWAES_decrypt aes_v8_decrypt
# define HWAES_cbc_encrypt aes_v8_cbc_encrypt
# define HWAES_ecb_encrypt aes_v8_ecb_encrypt
# if __ARM_MAX_ARCH__>=8 && (defined(__aarch64__) || defined(_M_ARM64))
# define HWAES_xts_encrypt aes_v8_xts_encrypt
# define HWAES_xts_decrypt aes_v8_xts_decrypt
# endif
# define HWAES_ctr32_encrypt_blocks aes_v8_ctr32_encrypt_blocks
# define AES_PMULL_CAPABLE ((OPENSSL_armcap_P & ARMV8_PMULL) && (OPENSSL_armcap_P & ARMV8_AES))
# define AES_GCM_ENC_BYTES 512
# define AES_GCM_DEC_BYTES 512
# if __ARM_MAX_ARCH__>=8 && (defined(__aarch64__) || defined(_M_ARM64))
# define AES_gcm_encrypt armv8_aes_gcm_encrypt
# define AES_gcm_decrypt armv8_aes_gcm_decrypt
# define AES_GCM_ASM(gctx) ((gctx)->ctr==aes_v8_ctr32_encrypt_blocks && \
(gctx)->gcm.funcs.ghash==gcm_ghash_v8)
/* The [unroll8_eor3_]aes_gcm_(enc|dec)_(128|192|256)_kernel() functions
* take input length in BITS and return number of BYTES processed */
size_t aes_gcm_enc_128_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t aes_gcm_enc_192_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t aes_gcm_enc_256_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t aes_gcm_dec_128_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t aes_gcm_dec_192_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t aes_gcm_dec_256_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_enc_128_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_enc_192_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_enc_256_kernel(const uint8_t *plaintext, uint64_t plaintext_length, uint8_t *ciphertext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_dec_128_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_dec_192_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t unroll8_eor3_aes_gcm_dec_256_kernel(const uint8_t *ciphertext, uint64_t plaintext_length, uint8_t *plaintext,
uint64_t *Xi, unsigned char ivec[16], const void *key);
size_t armv8_aes_gcm_encrypt(const unsigned char *in, unsigned char *out, size_t len, const void *key,
unsigned char ivec[16], u64 *Xi);
size_t armv8_aes_gcm_decrypt(const unsigned char *in, unsigned char *out, size_t len, const void *key,
unsigned char ivec[16], u64 *Xi);
void gcm_ghash_v8(u64 Xi[2],const u128 Htable[16],const u8 *inp, size_t len);
# endif
# endif
# endif
# endif /* OPENSSL_CPUID_OBJ */
# if defined(AES_ASM) && ( \
defined(__x86_64) || defined(__x86_64__) || \
defined(_M_AMD64) || defined(_M_X64) )
# define AES_CBC_HMAC_SHA_CAPABLE 1
# define AESNI_CBC_HMAC_SHA_CAPABLE (OPENSSL_ia32cap_P[1]&(1<<(57-32)))
# endif
# if defined(__loongarch__) || defined(__loongarch64)
# include "loongarch_arch.h"
# if defined(VPAES_ASM)
# define VPAES_CAPABLE (OPENSSL_loongarch_hwcap_P & LOONGARCH_HWCAP_LSX)
# endif
# endif
# if defined(AES_ASM) && !defined(I386_ONLY) && ( \
((defined(__i386) || defined(__i386__) || \
defined(_M_IX86)) && defined(OPENSSL_IA32_SSE2))|| \
defined(__x86_64) || defined(__x86_64__) || \
defined(_M_AMD64) || defined(_M_X64) )
/* AES-NI section */
# define AESNI_CAPABLE (OPENSSL_ia32cap_P[1]&(1<<(57-32)))
# ifdef VPAES_ASM
# define VPAES_CAPABLE (OPENSSL_ia32cap_P[1]&(1<<(41-32)))
# endif
# ifdef BSAES_ASM
# define BSAES_CAPABLE (OPENSSL_ia32cap_P[1]&(1<<(41-32)))
# endif
# define AES_GCM_ENC_BYTES 32
# define AES_GCM_DEC_BYTES 16
int aesni_set_encrypt_key(const unsigned char *userKey, int bits,
AES_KEY *key);
int aesni_set_decrypt_key(const unsigned char *userKey, int bits,
AES_KEY *key);
void aesni_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void aesni_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void aesni_ecb_encrypt(const unsigned char *in,
unsigned char *out,
size_t length, const AES_KEY *key, int enc);
void aesni_cbc_encrypt(const unsigned char *in,
unsigned char *out,
size_t length,
const AES_KEY *key, unsigned char *ivec, int enc);
# ifndef OPENSSL_NO_OCB
void aesni_ocb_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const void *key,
size_t start_block_num,
unsigned char offset_i[16],
const unsigned char L_[][16],
unsigned char checksum[16]);
void aesni_ocb_decrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const void *key,
size_t start_block_num,
unsigned char offset_i[16],
const unsigned char L_[][16],
unsigned char checksum[16]);
# endif /* OPENSSL_NO_OCB */
void aesni_ctr32_encrypt_blocks(const unsigned char *in,
unsigned char *out,
size_t blocks,
const void *key, const unsigned char *ivec);
void aesni_xts_encrypt(const unsigned char *in,
unsigned char *out,
size_t length,
const AES_KEY *key1, const AES_KEY *key2,
const unsigned char iv[16]);
void aesni_xts_decrypt(const unsigned char *in,
unsigned char *out,
size_t length,
const AES_KEY *key1, const AES_KEY *key2,
const unsigned char iv[16]);
void aesni_ccm64_encrypt_blocks(const unsigned char *in,
unsigned char *out,
size_t blocks,
const void *key,
const unsigned char ivec[16],
unsigned char cmac[16]);
void aesni_ccm64_decrypt_blocks(const unsigned char *in,
unsigned char *out,
size_t blocks,
const void *key,
const unsigned char ivec[16],
unsigned char cmac[16]);
# if defined(__x86_64) || defined(__x86_64__) || defined(_M_AMD64) || defined(_M_X64)
size_t aesni_gcm_encrypt(const unsigned char *in, unsigned char *out, size_t len,
const void *key, unsigned char ivec[16], u64 *Xi);
size_t aesni_gcm_decrypt(const unsigned char *in, unsigned char *out, size_t len,
const void *key, unsigned char ivec[16], u64 *Xi);
void gcm_ghash_avx(u64 Xi[2], const u128 Htable[16], const u8 *in, size_t len);
# define AES_gcm_encrypt aesni_gcm_encrypt
# define AES_gcm_decrypt aesni_gcm_decrypt
# define AES_GCM_ASM(ctx) (ctx->ctr == aesni_ctr32_encrypt_blocks && \
ctx->gcm.funcs.ghash == gcm_ghash_avx)
# endif
# elif defined(AES_ASM) && (defined(__sparc) || defined(__sparc__))
/* Fujitsu SPARC64 X support */
# include "crypto/sparc_arch.h"
# define SPARC_AES_CAPABLE (OPENSSL_sparcv9cap_P[1] & CFR_AES)
# define HWAES_CAPABLE (OPENSSL_sparcv9cap_P[0] & SPARCV9_FJAESX)
# define HWAES_set_encrypt_key aes_fx_set_encrypt_key
# define HWAES_set_decrypt_key aes_fx_set_decrypt_key
# define HWAES_encrypt aes_fx_encrypt
# define HWAES_decrypt aes_fx_decrypt
# define HWAES_cbc_encrypt aes_fx_cbc_encrypt
# define HWAES_ctr32_encrypt_blocks aes_fx_ctr32_encrypt_blocks
void aes_t4_set_encrypt_key(const unsigned char *key, int bits, AES_KEY *ks);
void aes_t4_set_decrypt_key(const unsigned char *key, int bits, AES_KEY *ks);
void aes_t4_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void aes_t4_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
/*
* Key-length specific subroutines were chosen for following reason.
* Each SPARC T4 core can execute up to 8 threads which share core's
* resources. Loading as much key material to registers allows to
* minimize references to shared memory interface, as well as amount
* of instructions in inner loops [much needed on T4]. But then having
* non-key-length specific routines would require conditional branches
* either in inner loops or on subroutines' entries. Former is hardly
* acceptable, while latter means code size increase to size occupied
* by multiple key-length specific subroutines, so why fight?
*/
void aes128_t4_cbc_encrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes128_t4_cbc_decrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes192_t4_cbc_encrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes192_t4_cbc_decrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes256_t4_cbc_encrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes256_t4_cbc_decrypt(const unsigned char *in, unsigned char *out,
size_t len, const AES_KEY *key,
unsigned char *ivec, int /*unused*/);
void aes128_t4_ctr32_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key,
unsigned char *ivec);
void aes192_t4_ctr32_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key,
unsigned char *ivec);
void aes256_t4_ctr32_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key,
unsigned char *ivec);
void aes128_t4_xts_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char *ivec);
void aes128_t4_xts_decrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char *ivec);
void aes256_t4_xts_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char *ivec);
void aes256_t4_xts_decrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char *ivec);
# elif defined(OPENSSL_CPUID_OBJ) && defined(__s390__)
/* IBM S390X support */
# include "s390x_arch.h"
/* Convert key size to function code: [16,24,32] -> [18,19,20]. */
# define S390X_AES_FC(keylen) (S390X_AES_128 + ((((keylen) << 3) - 128) >> 6))
/* Most modes of operation need km for partial block processing. */
# define S390X_aes_128_CAPABLE (OPENSSL_s390xcap_P.km[0] & \
S390X_CAPBIT(S390X_AES_128))
# define S390X_aes_192_CAPABLE (OPENSSL_s390xcap_P.km[0] & \
S390X_CAPBIT(S390X_AES_192))
# define S390X_aes_256_CAPABLE (OPENSSL_s390xcap_P.km[0] & \
S390X_CAPBIT(S390X_AES_256))
# define S390X_aes_128_cbc_CAPABLE 1 /* checked by callee */
# define S390X_aes_192_cbc_CAPABLE 1
# define S390X_aes_256_cbc_CAPABLE 1
# define S390X_aes_128_ecb_CAPABLE S390X_aes_128_CAPABLE
# define S390X_aes_192_ecb_CAPABLE S390X_aes_192_CAPABLE
# define S390X_aes_256_ecb_CAPABLE S390X_aes_256_CAPABLE
# define S390X_aes_128_ofb_CAPABLE (S390X_aes_128_CAPABLE && \
(OPENSSL_s390xcap_P.kmo[0] & \
S390X_CAPBIT(S390X_AES_128)))
# define S390X_aes_192_ofb_CAPABLE (S390X_aes_192_CAPABLE && \
(OPENSSL_s390xcap_P.kmo[0] & \
S390X_CAPBIT(S390X_AES_192)))
# define S390X_aes_256_ofb_CAPABLE (S390X_aes_256_CAPABLE && \
(OPENSSL_s390xcap_P.kmo[0] & \
S390X_CAPBIT(S390X_AES_256)))
# define S390X_aes_128_cfb_CAPABLE (S390X_aes_128_CAPABLE && \
(OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_128)))
# define S390X_aes_192_cfb_CAPABLE (S390X_aes_192_CAPABLE && \
(OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_192)))
# define S390X_aes_256_cfb_CAPABLE (S390X_aes_256_CAPABLE && \
(OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_256)))
# define S390X_aes_128_cfb8_CAPABLE (OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_128))
# define S390X_aes_192_cfb8_CAPABLE (OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_192))
# define S390X_aes_256_cfb8_CAPABLE (OPENSSL_s390xcap_P.kmf[0] & \
S390X_CAPBIT(S390X_AES_256))
# define S390X_aes_128_cfb1_CAPABLE 0
# define S390X_aes_192_cfb1_CAPABLE 0
# define S390X_aes_256_cfb1_CAPABLE 0
# define S390X_aes_128_ctr_CAPABLE 1 /* checked by callee */
# define S390X_aes_192_ctr_CAPABLE 1
# define S390X_aes_256_ctr_CAPABLE 1
# define S390X_aes_128_xts_CAPABLE 1 /* checked by callee */
# define S390X_aes_256_xts_CAPABLE 1
# define S390X_aes_128_gcm_CAPABLE (S390X_aes_128_CAPABLE && \
(OPENSSL_s390xcap_P.kma[0] & \
S390X_CAPBIT(S390X_AES_128)))
# define S390X_aes_192_gcm_CAPABLE (S390X_aes_192_CAPABLE && \
(OPENSSL_s390xcap_P.kma[0] & \
S390X_CAPBIT(S390X_AES_192)))
# define S390X_aes_256_gcm_CAPABLE (S390X_aes_256_CAPABLE && \
(OPENSSL_s390xcap_P.kma[0] & \
S390X_CAPBIT(S390X_AES_256)))
# define S390X_aes_128_ccm_CAPABLE (S390X_aes_128_CAPABLE && \
(OPENSSL_s390xcap_P.kmac[0] & \
S390X_CAPBIT(S390X_AES_128)))
# define S390X_aes_192_ccm_CAPABLE (S390X_aes_192_CAPABLE && \
(OPENSSL_s390xcap_P.kmac[0] & \
S390X_CAPBIT(S390X_AES_192)))
# define S390X_aes_256_ccm_CAPABLE (S390X_aes_256_CAPABLE && \
(OPENSSL_s390xcap_P.kmac[0] & \
S390X_CAPBIT(S390X_AES_256)))
# define S390X_CCM_AAD_FLAG 0x40
# ifndef OPENSSL_NO_OCB
# define S390X_aes_128_ocb_CAPABLE 0
# define S390X_aes_192_ocb_CAPABLE 0
# define S390X_aes_256_ocb_CAPABLE 0
# endif /* OPENSSL_NO_OCB */
# ifndef OPENSSL_NO_SIV
# define S390X_aes_128_siv_CAPABLE 0
# define S390X_aes_192_siv_CAPABLE 0
# define S390X_aes_256_siv_CAPABLE 0
# endif /* OPENSSL_NO_SIV */
/* Convert key size to function code: [16,24,32] -> [18,19,20]. */
# define S390X_AES_FC(keylen) (S390X_AES_128 + ((((keylen) << 3) - 128) >> 6))
# elif defined(OPENSSL_CPUID_OBJ) && defined(__riscv) && __riscv_xlen == 64
/* RISC-V 64 support */
# include "riscv_arch.h"
int rv64i_zkne_set_encrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
int rv64i_zknd_set_decrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
void rv64i_zkne_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void rv64i_zknd_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
# elif defined(OPENSSL_CPUID_OBJ) && defined(__riscv) && __riscv_xlen == 32
/* RISC-V 32 support */
# include "riscv_arch.h"
int rv32i_zkne_set_encrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
/* set_decrypt_key needs both zknd and zkne */
int rv32i_zknd_zkne_set_decrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
int rv32i_zbkb_zkne_set_encrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
int rv32i_zbkb_zknd_zkne_set_decrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
void rv32i_zkne_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void rv32i_zknd_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
# endif
# if defined(HWAES_CAPABLE)
int HWAES_set_encrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
int HWAES_set_decrypt_key(const unsigned char *userKey, const int bits,
AES_KEY *key);
void HWAES_encrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void HWAES_decrypt(const unsigned char *in, unsigned char *out,
const AES_KEY *key);
void HWAES_cbc_encrypt(const unsigned char *in, unsigned char *out,
size_t length, const AES_KEY *key,
unsigned char *ivec, const int enc);
void HWAES_ecb_encrypt(const unsigned char *in, unsigned char *out,
size_t length, const AES_KEY *key,
const int enc);
void HWAES_ctr32_encrypt_blocks(const unsigned char *in, unsigned char *out,
size_t len, const void *key,
const unsigned char ivec[16]);
void HWAES_xts_encrypt(const unsigned char *inp, unsigned char *out,
size_t len, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char iv[16]);
void HWAES_xts_decrypt(const unsigned char *inp, unsigned char *out,
size_t len, const AES_KEY *key1,
const AES_KEY *key2, const unsigned char iv[16]);
# ifndef OPENSSL_NO_OCB
# ifdef HWAES_ocb_encrypt
void HWAES_ocb_encrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const void *key,
size_t start_block_num,
unsigned char offset_i[16],
const unsigned char L_[][16],
unsigned char checksum[16]);
# else
# define HWAES_ocb_encrypt ((ocb128_f)NULL)
# endif
# ifdef HWAES_ocb_decrypt
void HWAES_ocb_decrypt(const unsigned char *in, unsigned char *out,
size_t blocks, const void *key,
size_t start_block_num,
unsigned char offset_i[16],
const unsigned char L_[][16],
unsigned char checksum[16]);
# else
# define HWAES_ocb_decrypt ((ocb128_f)NULL)
# endif
# endif /* OPENSSL_NO_OCB */
# endif /* HWAES_CAPABLE */
#endif /* OSSL_AES_PLATFORM_H */

View File

@ -1,51 +0,0 @@
/*
* Copyright 2006-2021 The OpenSSL Project Authors. All Rights Reserved.
* Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/* Copyright (c) 2017 National Security Research Institute. All rights reserved. */
#ifndef OSSL_CRYPTO_ARIA_H
# define OSSL_CRYPTO_ARIA_H
# pragma once
# include <openssl/opensslconf.h>
# ifdef OPENSSL_NO_ARIA
# error ARIA is disabled.
# endif
# define ARIA_ENCRYPT 1
# define ARIA_DECRYPT 0
# define ARIA_BLOCK_SIZE 16 /* Size of each encryption/decryption block */
# define ARIA_MAX_KEYS 17 /* Number of keys needed in the worst case */
typedef union {
unsigned char c[ARIA_BLOCK_SIZE];
unsigned int u[ARIA_BLOCK_SIZE / sizeof(unsigned int)];
} ARIA_u128;
typedef unsigned char ARIA_c128[ARIA_BLOCK_SIZE];
struct aria_key_st {
ARIA_u128 rd_key[ARIA_MAX_KEYS];
unsigned int rounds;
};
typedef struct aria_key_st ARIA_KEY;
int ossl_aria_set_encrypt_key(const unsigned char *userKey, const int bits,
ARIA_KEY *key);
int ossl_aria_set_decrypt_key(const unsigned char *userKey, const int bits,
ARIA_KEY *key);
void ossl_aria_encrypt(const unsigned char *in, unsigned char *out,
const ARIA_KEY *key);
#endif

View File

@ -1,153 +0,0 @@
/*
* Copyright 2015-2023 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#ifndef OSSL_CRYPTO_ASN1_H
# define OSSL_CRYPTO_ASN1_H
# pragma once
# include <openssl/asn1.h>
# include <openssl/core_dispatch.h> /* OSSL_FUNC_keymgmt_import() */
/* Internal ASN1 structures and functions: not for application use */
/* ASN1 public key method structure */
#include <openssl/core.h>
struct evp_pkey_asn1_method_st {
int pkey_id;
int pkey_base_id;
unsigned long pkey_flags;
char *pem_str;
char *info;
int (*pub_decode) (EVP_PKEY *pk, const X509_PUBKEY *pub);
int (*pub_encode) (X509_PUBKEY *pub, const EVP_PKEY *pk);
int (*pub_cmp) (const EVP_PKEY *a, const EVP_PKEY *b);
int (*pub_print) (BIO *out, const EVP_PKEY *pkey, int indent,
ASN1_PCTX *pctx);
int (*priv_decode) (EVP_PKEY *pk, const PKCS8_PRIV_KEY_INFO *p8inf);
int (*priv_encode) (PKCS8_PRIV_KEY_INFO *p8, const EVP_PKEY *pk);
int (*priv_print) (BIO *out, const EVP_PKEY *pkey, int indent,
ASN1_PCTX *pctx);
int (*pkey_size) (const EVP_PKEY *pk);
int (*pkey_bits) (const EVP_PKEY *pk);
int (*pkey_security_bits) (const EVP_PKEY *pk);
int (*param_decode) (EVP_PKEY *pkey,
const unsigned char **pder, int derlen);
int (*param_encode) (const EVP_PKEY *pkey, unsigned char **pder);
int (*param_missing) (const EVP_PKEY *pk);
int (*param_copy) (EVP_PKEY *to, const EVP_PKEY *from);
int (*param_cmp) (const EVP_PKEY *a, const EVP_PKEY *b);
int (*param_print) (BIO *out, const EVP_PKEY *pkey, int indent,
ASN1_PCTX *pctx);
int (*sig_print) (BIO *out,
const X509_ALGOR *sigalg, const ASN1_STRING *sig,
int indent, ASN1_PCTX *pctx);
void (*pkey_free) (EVP_PKEY *pkey);
int (*pkey_ctrl) (EVP_PKEY *pkey, int op, long arg1, void *arg2);
/* Legacy functions for old PEM */
int (*old_priv_decode) (EVP_PKEY *pkey,
const unsigned char **pder, int derlen);
int (*old_priv_encode) (const EVP_PKEY *pkey, unsigned char **pder);
/* Custom ASN1 signature verification */
int (*item_verify) (EVP_MD_CTX *ctx, const ASN1_ITEM *it, const void *data,
const X509_ALGOR *a, const ASN1_BIT_STRING *sig,
EVP_PKEY *pkey);
int (*item_sign) (EVP_MD_CTX *ctx, const ASN1_ITEM *it, const void *data,
X509_ALGOR *alg1, X509_ALGOR *alg2,
ASN1_BIT_STRING *sig);
int (*siginf_set) (X509_SIG_INFO *siginf, const X509_ALGOR *alg,
const ASN1_STRING *sig);
/* Check */
int (*pkey_check) (const EVP_PKEY *pk);
int (*pkey_public_check) (const EVP_PKEY *pk);
int (*pkey_param_check) (const EVP_PKEY *pk);
/* Get/set raw private/public key data */
int (*set_priv_key) (EVP_PKEY *pk, const unsigned char *priv, size_t len);
int (*set_pub_key) (EVP_PKEY *pk, const unsigned char *pub, size_t len);
int (*get_priv_key) (const EVP_PKEY *pk, unsigned char *priv, size_t *len);
int (*get_pub_key) (const EVP_PKEY *pk, unsigned char *pub, size_t *len);
/* Exports and imports to / from providers */
size_t (*dirty_cnt) (const EVP_PKEY *pk);
int (*export_to) (const EVP_PKEY *pk, void *to_keydata,
OSSL_FUNC_keymgmt_import_fn *importer,
OSSL_LIB_CTX *libctx, const char *propq);
OSSL_CALLBACK *import_from;
int (*copy) (EVP_PKEY *to, EVP_PKEY *from);
int (*priv_decode_ex) (EVP_PKEY *pk,
const PKCS8_PRIV_KEY_INFO *p8inf,
OSSL_LIB_CTX *libctx,
const char *propq);
} /* EVP_PKEY_ASN1_METHOD */ ;
DEFINE_STACK_OF_CONST(EVP_PKEY_ASN1_METHOD)
extern const EVP_PKEY_ASN1_METHOD ossl_dh_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_dhx_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_dsa_asn1_meths[5];
extern const EVP_PKEY_ASN1_METHOD ossl_eckey_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_ecx25519_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_ecx448_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_ed25519_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_ed448_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_sm2_asn1_meth;
extern const EVP_PKEY_ASN1_METHOD ossl_rsa_asn1_meths[2];
extern const EVP_PKEY_ASN1_METHOD ossl_rsa_pss_asn1_meth;
/*
* These are used internally in the ASN1_OBJECT to keep track of whether the
* names and data need to be free()ed
*/
# define ASN1_OBJECT_FLAG_DYNAMIC 0x01/* internal use */
# define ASN1_OBJECT_FLAG_CRITICAL 0x02/* critical x509v3 object id */
# define ASN1_OBJECT_FLAG_DYNAMIC_STRINGS 0x04/* internal use */
# define ASN1_OBJECT_FLAG_DYNAMIC_DATA 0x08/* internal use */
struct asn1_object_st {
const char *sn, *ln;
int nid;
int length;
const unsigned char *data; /* data remains const after init */
int flags; /* Should we free this one */
};
/* ASN1 print context structure */
struct asn1_pctx_st {
unsigned long flags;
unsigned long nm_flags;
unsigned long cert_flags;
unsigned long oid_flags;
unsigned long str_flags;
} /* ASN1_PCTX */ ;
/* ASN1 type functions */
int ossl_asn1_type_set_octetstring_int(ASN1_TYPE *a, long num,
unsigned char *data, int len);
int ossl_asn1_type_get_octetstring_int(const ASN1_TYPE *a, long *num,
unsigned char *data, int max_len);
int ossl_x509_algor_new_from_md(X509_ALGOR **palg, const EVP_MD *md);
const EVP_MD *ossl_x509_algor_get_md(X509_ALGOR *alg);
X509_ALGOR *ossl_x509_algor_mgf1_decode(X509_ALGOR *alg);
int ossl_x509_algor_md_to_mgf1(X509_ALGOR **palg, const EVP_MD *mgf1md);
int ossl_asn1_time_print_ex(BIO *bp, const ASN1_TIME *tm, unsigned long flags);
EVP_PKEY *ossl_d2i_PrivateKey_legacy(int keytype, EVP_PKEY **a,
const unsigned char **pp, long length,
OSSL_LIB_CTX *libctx, const char *propq);
X509_ALGOR *ossl_X509_ALGOR_from_nid(int nid, int ptype, void *pval);
time_t ossl_asn1_string_to_time_t(const char *asn1_string);
void ossl_asn1_string_set_bits_left(ASN1_STRING *str, unsigned int num);
#endif /* ndef OSSL_CRYPTO_ASN1_H */

View File

@ -1,24 +0,0 @@
/*
* Copyright 2019-2021 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#ifndef OSSL_CRYPTO_ASN1_DSA_H
# define OSSL_CRYPTO_ASN1_DSA_H
# pragma once
#include "internal/packet.h"
int ossl_encode_der_length(WPACKET *pkt, size_t cont_len);
int ossl_encode_der_integer(WPACKET *pkt, const BIGNUM *n);
int ossl_encode_der_dsa_sig(WPACKET *pkt, const BIGNUM *r, const BIGNUM *s);
int ossl_decode_der_length(PACKET *pkt, PACKET *subpkt);
int ossl_decode_der_integer(PACKET *pkt, BIGNUM *n);
size_t ossl_decode_der_dsa_sig(BIGNUM *r, BIGNUM *s, const unsigned char **ppin,
size_t len);
#endif

View File

@ -1,27 +0,0 @@
/*
* Generated by util/mkerr.pl DO NOT EDIT
* Copyright 2020-2021 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#ifndef OSSL_CRYPTO_ASN1ERR_H
# define OSSL_CRYPTO_ASN1ERR_H
# pragma once
# include <openssl/opensslconf.h>
# include <openssl/symhacks.h>
# ifdef __cplusplus
extern "C" {
# endif
int ossl_err_load_ASN1_strings(void);
# ifdef __cplusplus
}
# endif
#endif

View File

@ -1,19 +0,0 @@
/*
* Copyright 2016-2021 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#ifndef OSSL_CRYPTO_ASYNC_H
# define OSSL_CRYPTO_ASYNC_H
# pragma once
# include <openssl/async.h>
int async_init(void);
void async_deinit(void);
#endif

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