forked from cory/tildefriends
Compare commits
164 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca28b5ca82 | |||
19e26c1759 | |||
790f6643a4 | |||
2158ad3c0b | |||
d904d8922f | |||
da50792500 | |||
b4629acc48 | |||
0cf4118330 | |||
dd61a6ecc3 | |||
8e6f1284e1 | |||
813d3cd492 | |||
f421606e21 | |||
1ccb9183b4 | |||
7d9b627f37 | |||
3038138909 | |||
2ca08d21e4 | |||
478e96fc5f | |||
e237c7ea1d | |||
bf9ff088fd | |||
e073ebedd1 | |||
10d4ae7dcc | |||
5b8bdbb3e4 | |||
c807e21c6b | |||
cc92d0e316 | |||
09c396d5a3 | |||
bc5bbca951 | |||
ed4faedcd7 | |||
251556ebed | |||
1324afb459 | |||
1119804fc2 | |||
cdf6440197 | |||
8727fe00af | |||
7da7890bb6 | |||
706bd2c51f | |||
acabec940e | |||
470b998b61 | |||
80fad05f23 | |||
07a912fb9a | |||
e9d83262c4 | |||
74323c22f9 | |||
2614e89b1b | |||
e092fe1399 | |||
9cbe895cb8 | |||
b0b0f74e83 | |||
d9eaa92c37 | |||
566d07117e | |||
2bffdb1168 | |||
1359b48c9f | |||
a69fb5eeac | |||
38e313350e | |||
5052dc04f2 | |||
9ef3a3aca0 | |||
7b91a2ec37 | |||
2926f855a1 | |||
639419db60 | |||
54747c127c | |||
791c3dd787 | |||
b00d75ab7c | |||
956ea0df56 | |||
30014040e7 | |||
ab055c3394 | |||
1e37eeea05 | |||
84aec0278d | |||
06642f58c5 | |||
e6d44b32f4 | |||
1f3f6e2b92 | |||
8f2d3e3bcd | |||
2df2fc5792 | |||
20b0337e0a | |||
e86b9dae48 | |||
71de897419 | |||
3edfaf9137 | |||
19c1784864 | |||
0d9fac7363 | |||
2fb91fccc0 | |||
24e1ab12ab | |||
10ea885d8d | |||
ec65faa12d | |||
53692a1ea8 | |||
ebef51b4ea | |||
a94d6f9271 | |||
3d2c88c201 | |||
bdeee7fc0e | |||
33a037e0ea | |||
2dc2d9ebf6 | |||
9748f0ed8b | |||
d6be2f7d54 | |||
63615747a7 | |||
fbb657a85c | |||
bdac0c7879 | |||
54dde76a8a | |||
2bbe22bc7a | |||
ad8532f7ac | |||
602941104e | |||
d38b41687c | |||
08125cd1e8 | |||
2ce2097a3f | |||
a5da17e1b1 | |||
2b0962f087 | |||
37173cce4c | |||
37edbd9824 | |||
a32bb02223 | |||
2ab1b84432 | |||
52ae19220c | |||
10bfa65a4e | |||
2a3b1a1e33 | |||
f74f4f6da9 | |||
12a8b7a058 | |||
400f07660f | |||
d532795b7f | |||
6064ed6a3a | |||
2c1a43df2e | |||
bf72782c9f | |||
63dcab30c3 | |||
50e48af7c4 | |||
9127a18ff0 | |||
61ff466908 | |||
1c10768aa4 | |||
992b123853 | |||
f736756b20 | |||
28d73f5b37 | |||
262b0e5e52 | |||
1e3807bcb9 | |||
2ed3295f77 | |||
8c9d687d50 | |||
b8b694864e | |||
961109635b | |||
86bc46a11e | |||
a6a6fe75ec | |||
f55f863867 | |||
4ce988d00b | |||
1548a8a852 | |||
a9551b057b | |||
88c7d91858 | |||
53cb80ebf7 | |||
1f67343d75 | |||
4bea8bb6ba | |||
8e1461b3f1 | |||
90b513d070 | |||
8a2d3d4669 | |||
1741403206 | |||
980db880cc | |||
507a62539d | |||
6b5d73ed5c | |||
1f77df7a90 | |||
fa87462405 | |||
a5f9f927e6 | |||
b35d74ce36 | |||
ac60be14a5 | |||
beda047eb0 | |||
f6742bebf3 | |||
7f334ad783 | |||
ffda896308 | |||
b2fbe9dfac | |||
6d6c41bffa | |||
e04d137af5 | |||
ec52e62908 | |||
6104af0d70 | |||
0ca05e297d | |||
e0dcec074c | |||
a8cecb5c64 | |||
582ee0e4d7 | |||
0ba54c2b7b | |||
3c288f7f68 |
@ -2,8 +2,10 @@ FROM bitnami/minideb:bullseye AS build
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
libssl-dev
|
||||
gcc \
|
||||
libc6-dev \
|
||||
libssl-dev \
|
||||
make
|
||||
|
||||
COPY . /app
|
||||
RUN make -C /app -j $(nproc) release
|
||||
|
253
Makefile
253
Makefile
@ -5,7 +5,7 @@ MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
PROJECT = tildefriends
|
||||
BUILD_DIR ?= out
|
||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease
|
||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
|
||||
UNAME_M := $(shell uname -m)
|
||||
|
||||
CFLAGS += \
|
||||
@ -16,17 +16,54 @@ CFLAGS += \
|
||||
-MMD \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-fno-omit-frame-pointer \
|
||||
-fno-exceptions \
|
||||
-g
|
||||
LDFLAGS += -Wl,--gc-sections
|
||||
|
||||
NDK_PATH := /usr/lib/android-sdk/ndk-bundle
|
||||
NDK_API_VERSION := 30
|
||||
NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||
ANDROID_SDK ?= ~/Android/Sdk
|
||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
|
||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
|
||||
ANDROID_NDK_API_VERSION := 31
|
||||
ANDROID_MIN_SDK_VERSION := 26
|
||||
|
||||
debug windebug androiddebug: CFLAGS += -Og
|
||||
debug release androidrelease: LDFLAGS += -rdynamic
|
||||
release winrelease: CFLAGS += -DNDEBUG -O3
|
||||
ANDROID_ARM64_TARGETS := \
|
||||
out/androiddebug/tildefriends \
|
||||
out/androidrelease/tildefriends
|
||||
ANDROID_X86_64_TARGETS := \
|
||||
out/androiddebug-x86_64/tildefriends \
|
||||
out/androidrelease-x86_64/tildefriends
|
||||
ANDROID_TARGETS := \
|
||||
$(ANDROID_X86_64_TARGETS) \
|
||||
$(ANDROID_ARM64_TARGETS)
|
||||
|
||||
DEBUG_TARGETS := \
|
||||
out/debug/tildefriends \
|
||||
out/windebug/tildefriends \
|
||||
out/androiddebug/tildefriends \
|
||||
out/androiddebug-x86_64/tildefriends
|
||||
RELEASE_TARGETS := \
|
||||
out/release/tildefriends \
|
||||
out/winrelease/tildefriends \
|
||||
out/androidrelease/tildefriends \
|
||||
out/androidrelease-x86_64/tildefriends
|
||||
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
|
||||
|
||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
|
||||
$(ANDROID_TARGETS): CFLAGS += \
|
||||
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||
-fPIC \
|
||||
-fdebug-compilation-dir . \
|
||||
-fomit-frame-pointer \
|
||||
-fno-asynchronous-unwind-tables
|
||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
|
||||
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
||||
windebug winrelease: AS = $(CC)
|
||||
windebug winrelease: CFLAGS += \
|
||||
@ -38,15 +75,17 @@ windebug winrelease: LDFLAGS += \
|
||||
-static \
|
||||
-lm \
|
||||
-Ldeps/openssl/mingw64/lib
|
||||
androiddebug androidrelease: CC = $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
||||
androiddebug androidrelease: AS = $(CC)
|
||||
androiddebug androidrelease: CFLAGS += \
|
||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
||||
-Ideps/openssl/android/arm64-v8a/usr/local/include \
|
||||
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
||||
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
||||
$(ANDROID_TARGETS): AS = $(CC)
|
||||
$(ANDROID_TARGETS): CFLAGS += \
|
||||
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
||||
-Wno-unknown-warning-option
|
||||
androiddebug androidrelease: LDFLAGS += \
|
||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
||||
-Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
||||
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||
@ -57,8 +96,8 @@ get_objs = \
|
||||
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
||||
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
|
||||
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
||||
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
||||
|
||||
APP_SOURCES := $(wildcard src/*.c)
|
||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||
@ -69,18 +108,16 @@ $(APP_OBJS): CFLAGS += \
|
||||
-Ideps/libsodium \
|
||||
-Ideps/libsodium/src/libsodium/include \
|
||||
-Ideps/libuv/include \
|
||||
-Ideps/zlib \
|
||||
-Ideps/zlib/contrib/minizip \
|
||||
-Ideps/picohttpparser \
|
||||
-Ideps/quickjs \
|
||||
-Ideps/sqlite \
|
||||
-Ideps/valgrind \
|
||||
-Ideps/xopt \
|
||||
-Wdouble-promotion \
|
||||
-Werror
|
||||
|
||||
BASE64C_SOURCES := deps/base64c/src/base64c.c
|
||||
BASE64C_OBJS := $(call get_objs,BASE64C_SOURCES)
|
||||
$(BASE64C_OBJS): CFLAGS += \
|
||||
-Wno-sign-compare
|
||||
|
||||
BLOWFISH_SOURCES := \
|
||||
deps/crypt_blowfish/crypt_blowfish.c \
|
||||
deps/crypt_blowfish/crypt_gensalt.c \
|
||||
@ -105,13 +142,10 @@ UV_SOURCES_unix := \
|
||||
deps/libuv/src/unix/async.c \
|
||||
deps/libuv/src/unix/core.c \
|
||||
deps/libuv/src/unix/dl.c \
|
||||
deps/libuv/src/unix/epoll.c \
|
||||
deps/libuv/src/unix/fs.c \
|
||||
deps/libuv/src/unix/getaddrinfo.c \
|
||||
deps/libuv/src/unix/getnameinfo.c \
|
||||
deps/libuv/src/unix/linux-core.c \
|
||||
deps/libuv/src/unix/linux-inotify.c \
|
||||
deps/libuv/src/unix/linux-syscalls.c \
|
||||
deps/libuv/src/unix/linux.c \
|
||||
deps/libuv/src/unix/loop-watcher.c \
|
||||
deps/libuv/src/unix/loop.c \
|
||||
deps/libuv/src/unix/pipe.c \
|
||||
@ -129,7 +163,6 @@ UV_SOURCES_unix := \
|
||||
deps/libuv/src/unix/tty.c \
|
||||
deps/libuv/src/unix/udp.c
|
||||
UV_SOURCES_android := \
|
||||
deps/libuv/src/unix/pthread-fixes.c \
|
||||
deps/libuv/src/unix/random-getentropy.c
|
||||
UV_SOURCES_win := \
|
||||
deps/libuv/src/win/async.c \
|
||||
@ -161,12 +194,13 @@ UV_OBJS := $(call get_objs,UV_SOURCES)
|
||||
$(UV_OBJS): CFLAGS += \
|
||||
-Ideps/libuv/include \
|
||||
-Ideps/libuv/src \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-incompatible-pointer-types \
|
||||
-Wno-sign-compare \
|
||||
-Wno-unused-variable \
|
||||
-Wno-dangling-pointer \
|
||||
-Wno-incompatible-pointer-types \
|
||||
-Wno-maybe-uninitialized \
|
||||
-Wno-sign-compare \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-unused-result \
|
||||
-Wno-unused-variable \
|
||||
-D_GNU_SOURCE
|
||||
|
||||
SODIUM_SOURCES := \
|
||||
@ -207,6 +241,7 @@ SODIUM_SOURCES := \
|
||||
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
||||
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
||||
deps/libsodium/src/libsodium/sodium/core.c \
|
||||
deps/libsodium/src/libsodium/sodium/codecs.c \
|
||||
deps/libsodium/src/libsodium/sodium/runtime.c \
|
||||
deps/libsodium/src/libsodium/sodium/utils.c
|
||||
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
||||
@ -223,23 +258,37 @@ SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||
$(SQLITE_OBJS): CFLAGS += \
|
||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||
-DSQLITE_DEFAULT_MEMSTATUS=0 \
|
||||
-DSQLITE_DQS=0 \
|
||||
-DSQLITE_ENABLE_MEMSYS5 \
|
||||
-DSQLITE_ENABLE_FTS5 \
|
||||
-DSQLITE_ENABLE_JSON1 \
|
||||
-DSQLITE_MAX_LENGTH=5242880 \
|
||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||
-DSQLITE_MAX_COLUMN=100 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||
-DSQLITE_MAX_ATTACHED=0 \
|
||||
-DSQLITE_MAX_COLUMN=100 \
|
||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||
-DSQLITE_MAX_LENGTH=5242880 \
|
||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||
-DSQLITE_OMIT_DEPRECATED \
|
||||
-DSQLITE_OMIT_DESERIALIZE \
|
||||
-DSQLITE_OMIT_LOAD_EXTENSION \
|
||||
-DSQLITE_OMIT_TCL_VARIABLE \
|
||||
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||
-DSQLITE_SECURE_DELETE \
|
||||
-DSQLITE_THREADSAFE=0 \
|
||||
-DSQLITE_UNTESTABLE \
|
||||
-DSQLITE_USE_ALLOCA \
|
||||
-DHAVE_ISNAN \
|
||||
-Wno-implicit-fallthrough \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-unused-function
|
||||
-Wno-unused-function \
|
||||
-Wno-unused-variable
|
||||
|
||||
XOPT_SOURCES := deps/xopt/xopt.c
|
||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
||||
@ -249,25 +298,27 @@ $(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
||||
-DHAVE_VASNPRINTF \
|
||||
-DHAVE_VASPRINTF \
|
||||
-Dvsnprintf=rpl_vsnprintf
|
||||
$(XOPT_OBJS): CFLAGS += \
|
||||
-Wno-implicit-const-int-float-conversion
|
||||
|
||||
QUICKJS_SOURCES := \
|
||||
deps/quickjs/cutils.c \
|
||||
deps/quickjs/libbf.c \
|
||||
deps/quickjs/libregexp.c \
|
||||
deps/quickjs/libunicode.c \
|
||||
deps/quickjs/quickjs-libc.c \
|
||||
deps/quickjs/quickjs.c
|
||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||
$(QUICKJS_OBJS): CFLAGS += \
|
||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||
-DCONFIG_BIGNUM \
|
||||
-DDUMP_LEAKS \
|
||||
-D_GNU_SOURCE \
|
||||
-Wno-sign-compare \
|
||||
-Wno-enum-conversion \
|
||||
-Wno-implicit-const-int-float-conversion \
|
||||
-Wno-implicit-fallthrough \
|
||||
-Wno-unused-variable \
|
||||
-Wno-sign-compare \
|
||||
-Wno-unused-but-set-variable \
|
||||
-Wno-enum-conversion
|
||||
-Wno-unused-variable
|
||||
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||
|
||||
LIBBACKTRACE_SOURCES := \
|
||||
deps/libbacktrace/atomic.c \
|
||||
@ -299,7 +350,20 @@ $(LIBBACKTRACE_OBJS): CFLAGS += \
|
||||
PICOHTTPPARSER_SOURCES := \
|
||||
deps/picohttpparser/picohttpparser.c
|
||||
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
||||
# $(PICOHTTPPARSER_OBJS): CFLAGS +=
|
||||
|
||||
MINIUNZIP_SOURCES := \
|
||||
deps/zlib/contrib/minizip/unzip.c \
|
||||
deps/zlib/contrib/minizip/ioapi.c \
|
||||
deps/zlib/adler32.c \
|
||||
deps/zlib/crc32.c \
|
||||
deps/zlib/inffast.c \
|
||||
deps/zlib/inflate.c \
|
||||
deps/zlib/inftrees.c \
|
||||
deps/zlib/zutil.c
|
||||
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
|
||||
$(MINIUNZIP_OBJS): CFLAGS += \
|
||||
-Ideps/zlib \
|
||||
-Wno-maybe-uninitialized
|
||||
|
||||
LDFLAGS += \
|
||||
-pthread \
|
||||
@ -309,30 +373,34 @@ debug release: LDFLAGS += \
|
||||
-lssl \
|
||||
-lcrypto
|
||||
windebug winrelease: LDFLAGS += \
|
||||
-lwsock32 \
|
||||
-lws2_32 \
|
||||
-lkernel32 \
|
||||
-liphlpapi \
|
||||
-luserenv \
|
||||
-lssl \
|
||||
-lcrypto \
|
||||
-lcrypt32 \
|
||||
-ldbghelp \
|
||||
-liphlpapi \
|
||||
-lkernel32 \
|
||||
-lole32 \
|
||||
-luserenv \
|
||||
-luuid \
|
||||
-lws2_32 \
|
||||
-lcrypt32
|
||||
androiddebug androidrelease: LDFLAGS += \
|
||||
-lwsock32
|
||||
$(ANDROID_TARGETS): LDFLAGS += \
|
||||
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
||||
-ldl \
|
||||
-llog \
|
||||
-lssl \
|
||||
-lcrypto
|
||||
|
||||
unix: debug release
|
||||
win: windebug winrelease
|
||||
all: $(BUILD_TYPES)
|
||||
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
|
||||
.PHONY: all win unix
|
||||
|
||||
ALL_APP_OBJS := \
|
||||
$(APP_OBJS) \
|
||||
$(BASE64C_OBJS) \
|
||||
$(BLOWFISH_OBJS) \
|
||||
$(LIBBACKTRACE_OBJS) \
|
||||
$(MINIUNZIP_OBJS) \
|
||||
$(PICOHTTPPARSER_OBJS) \
|
||||
$(QUICKJS_OBJS) \
|
||||
$(SODIUM_OBJS) \
|
||||
@ -349,7 +417,7 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
||||
|
||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||
@echo [link] $$@
|
||||
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
|
||||
|
||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||
@mkdir -p $$(dir $$@)
|
||||
@ -364,6 +432,77 @@ endef
|
||||
|
||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||
|
||||
# Android support.
|
||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||
@mkdir -p $(dir $@)
|
||||
@echo [aapt2] $@
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||
|
||||
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||
@mkdir -p $(dir $@)
|
||||
@echo [aapt2] $@
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||
|
||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||
@mkdir -p $(dir $@)
|
||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||
|
||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||
|
||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||
@echo [javac] $(CLASS_FILES)
|
||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||
|
||||
out/apk/classes.dex: $(CLASS_FILES)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo [d8] $@
|
||||
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||
|
||||
PACKAGE_DIRS := \
|
||||
apps/ \
|
||||
core/ \
|
||||
deps/codemirror/ \
|
||||
deps/lit/ \
|
||||
deps/split/ \
|
||||
deps/smoothie/
|
||||
|
||||
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
|
||||
|
||||
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
|
||||
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
|
||||
|
||||
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||
|
||||
out/%.unsigned.apk:
|
||||
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||
@echo [aapt] $@
|
||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
|
||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
|
||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
|
||||
@cp out/apk/res.apk $@
|
||||
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
|
||||
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
|
||||
|
||||
out/%.apk: out/apk/%.unsigned.apk
|
||||
@echo [apksigner] $(notdir $@)
|
||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
||||
|
||||
apk: out/TildeFriends-debug.apk
|
||||
.PHONY: apk
|
||||
|
||||
apkgo: out/TildeFriends-debug.apk
|
||||
@adb install $<
|
||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||
.PHONY: apkgo
|
||||
|
||||
apklog:
|
||||
@adb logcat *:S tildefriends
|
||||
.PHONY: apklog
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
.PHONY: clean
|
||||
|
@ -28,7 +28,7 @@ privileges. Further administration can be done at
|
||||
<http://localhost:12345/~core/admin/`>.
|
||||
|
||||
## Documentation
|
||||
There are the very beginnings of developer documentation in `apps/cory/docs/`
|
||||
There are the very beginnings of developer documentation in `apps/docs/`
|
||||
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
||||
|
||||
## License
|
||||
|
4
apps/admin.json
Normal file
4
apps/admin.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🎛"
|
||||
}
|
4
apps/api.json
Normal file
4
apps/api.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📜"
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
var global = Function('return this')();
|
||||
function treeify(o) {
|
||||
if (typeof(o) == 'object') {
|
||||
return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
|
||||
@ -8,4 +7,4 @@ function treeify(o) {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(global), null, 2)}</pre>`);
|
||||
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(globalThis), null, 2)}</pre>`);
|
4
apps/apps.json
Normal file
4
apps/apps.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💻"
|
||||
}
|
77
apps/apps/app.js
Normal file
77
apps/apps/app.js
Normal file
@ -0,0 +1,77 @@
|
||||
async function fetch_info(apps) {
|
||||
let result = {};
|
||||
for (let [key, value] of Object.entries(apps)) {
|
||||
let blob = await ssb.blobGet(value);
|
||||
blob = blob ? utf8Decode(blob) : '{}';
|
||||
result[key] = JSON.parse(blob);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
var apps = await fetch_info(await core.apps());
|
||||
var core_apps = await fetch_info(await core.apps('core'));
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 64px);
|
||||
justify-content: space-around;
|
||||
}
|
||||
.app {
|
||||
height: 96px;
|
||||
width: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.app > a {
|
||||
text-decoration: none;
|
||||
max-width: 64px;
|
||||
text-overflow: ellipsis ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: #888">
|
||||
<h1 id="apps_title">Apps</h1>
|
||||
<div id="apps" class="container"></div>
|
||||
<h1>Core Apps</h1>
|
||||
<div id="core_apps" class="container"></div>
|
||||
</body>
|
||||
<script>
|
||||
function populate_apps(id, name, apps) {
|
||||
var list = document.getElementById(id);
|
||||
for (let app of Object.keys(apps).sort()) {
|
||||
let div = list.appendChild(document.createElement('div'));
|
||||
div.classList.add('app');
|
||||
|
||||
let icon_a = document.createElement('a');
|
||||
let icon = document.createElement('div');
|
||||
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||
icon.style.fontSize = 'xxx-large';
|
||||
icon_a.appendChild(icon);
|
||||
icon_a.href = '/~' + name + '/' + app + '/';
|
||||
icon_a.target = '_top';
|
||||
div.appendChild(icon_a);
|
||||
|
||||
let a = document.createElement('a');
|
||||
a.appendChild(document.createTextNode(app));
|
||||
a.href = '/~' + name + '/' + app + '/';
|
||||
a.target = '_top';
|
||||
div.appendChild(a);
|
||||
}
|
||||
}
|
||||
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||
</script>
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
main();
|
4
apps/appstore.json
Normal file
4
apps/appstore.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🛍"
|
||||
}
|
55
apps/appstore/app.js
Normal file
55
apps/appstore/app.js
Normal file
@ -0,0 +1,55 @@
|
||||
async function get_apps() {
|
||||
let results = {};
|
||||
await ssb.sqlAsync(`
|
||||
SELECT messages.*
|
||||
FROM messages_fts('"application/tildefriends"')
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
ORDER BY timestamp
|
||||
`,
|
||||
[],
|
||||
function(row) {
|
||||
let content = JSON.parse(row.content);
|
||||
for (let mention of content.mentions) {
|
||||
if (mention?.type === 'application/tildefriends') {
|
||||
results[JSON.stringify([row.author, mention.name])] = {
|
||||
message: row,
|
||||
blob: mention.link,
|
||||
name: mention.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
|
||||
}
|
||||
|
||||
function render_app(app) {
|
||||
return `
|
||||
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
|
||||
<a href="/~cory/ssb/#${app.message.author}">@</a>
|
||||
<a href="/~cory/ssb/#${app.message.id}">%</a>
|
||||
<a href="/${app.blob}/">${app.name}</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let apps = await get_apps();
|
||||
app.setDocument(`
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top">
|
||||
<style>
|
||||
a:link { color: #bbf; }
|
||||
a:visited { color: #ddd; }
|
||||
a:hover { color: #ddf; }
|
||||
</style>
|
||||
</head>
|
||||
<body style="color: #fff">
|
||||
<h1>${apps.length} apps</h1>
|
||||
${apps.map(render_app).join('\n')}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
main();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&uhGJsy5+qBgOgEgMqCTDasK+C+GWGptHKfPiAsD5eGA=.sha256","index.html":"&D3JwdPXy/QsLXkmwNDrBFXdzxfqO1/JGxfqEArnS5v4=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&TZ2ymD6cFVUjQleGcDslt8apjp7k3xLlfv2F8rQVM4I=.sha256"}}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&qEJDfZ43KazIxiZl8OCKb2uaDOsPkxnIohEzQ1LLFpg=.sha256"}}
|
@ -1,31 +0,0 @@
|
||||
async function main() {
|
||||
var apps = await core.apps();
|
||||
var core_apps = await core.apps('core');
|
||||
var doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="background: #888">
|
||||
<h1>Apps</h1>
|
||||
<ul id="apps"></ul>
|
||||
<h1>Core Apps</h1>
|
||||
<ul id="core_apps"></ul>
|
||||
</body>
|
||||
<script>
|
||||
function populate_apps(id, name, apps) {
|
||||
var list = document.getElementById(id);
|
||||
for (let app of Object.keys(apps).sort()) {
|
||||
var li = list.appendChild(document.createElement('li'));
|
||||
var a = document.createElement('a');
|
||||
a.innerText = app;
|
||||
a.href = '/~' + name + '/' + app + '/';
|
||||
a.target = '_top';
|
||||
li.appendChild(a);
|
||||
}
|
||||
}
|
||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||
</script>
|
||||
</html>`
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
main();
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&WEvJYebSMi5d2eXgUwJJmvR/Q4slFg3zHYB8Q2mXJII=.sha256","index.md":"&79+ntX4sRvg+MboV5nMFz01BSicxsWIQRx719VHS8uk=.sha256","todo.md":"&hQABwP24zFFhdHagRMF3Am7rV2yH19e+0xJ4wnZ4kfM=.sha256","structure.md":"&jph8x/fMXKOd4I0ZiUVb0ZLTfPQ7gBWoxJPrvtX6vtw=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","ssb.md":"&JH1JfoTaCcUifCpnAwhImKBACI0PHoLhoOw1WAnWpLw=.sha256","vision.md":"&v2wu2MGlhNvaALQQ9rGna7ZeEQWSghFgQcDfD5xEyE0=.sha256"}}
|
@ -1,17 +0,0 @@
|
||||
# ID Refactor
|
||||
[Back to index](#index)
|
||||
|
||||
## Goals
|
||||
- no way to get private key in javascript
|
||||
- ssb.c syncs/broadcasts/... efficiently for everybody
|
||||
|
||||
## Schema
|
||||
- separate table to discourage leakage
|
||||
- `CREATE TABLE identities (user TEXT, public TEXT, secret TEXT);`
|
||||
|
||||
## API
|
||||
- `ssb.createIdentity()` -> `id`
|
||||
- `ssb.getIdentities()` => `[id, ...]`
|
||||
- `ssb.deleteIdentity(id)`
|
||||
- `ssb.post(id, ...)`
|
||||
- `ssb.appendMessage(id, ...)`
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&3d9ABFgRwQvWsYbFv/rzimtnLDnVrWlGtdw7serFIGw=.sha256"}}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&1HWTkyCc1doft6dyKF5FDxtRAErNeY25CBrfZbKPpyo=.sha256","lit-all.min.js":"&XKgdRySJuiZeZvchNFGjVWn0XOVhQFmG7/HTWYQ8s68=.sha256","index.html":"&TxhFekB9ov7tf/fmkAg7x5797i27oLidhgxEfDKC0T0=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&lA9iFp1YbqSndxXZuwtgmrj7NDMkN71nJITbtjWL3VA=.sha256","tf-id-picker.js":"&maN8DUFrmRxW5nsVyOAMk5k1ekcz/pfzvSS99ac3jo8=.sha256","tf-app.js":"&F0fyawIO410YFidrzFjlHeY++sZy6ledf6CAXB+45U4=.sha256","tf-message.js":"&HToh+7UCoanBzlr/TEsy/JG4OS2IBU1tMuzjuNmUkAo=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&lYNeL7cVlDgcqrfkoRIe69DHZeqSZMiHhZIieblHbU0=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&7HZLHf5NB5hE6FW0hiXNvM17ekGBn5BBle1bvnjVjyo=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&tOkUocccQWBzkNzSEf9VMltkTSHcUALYSPYVWmJMoBc=.sha256","tf-styles.js":"&LFeL/vWgrv4N8q/mBrQAnhbaOI+dXNJYvH9bn1bXSqQ=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&fY+thANurOKU2/RhDt411ZtkxW0nV24+hLEf00Z1sTY=.sha256","tf-tab-connections.js":"&ywqBz3w63R6naH09kZ+01A0SfmtuSfk8QPBXWsli0yg=.sha256","tf-news.js":"&Zn+vxLUqVJbo/q6RcW8ezvbdilzllvXhZRyXk8kYwL0=.sha256","tribute.css":"&9FogMzZHKXCfGb7mlh7z+/wiNZzBsOB/tKoh6MfYJno=.sha256","tribute.esm.js":"&P1wKqCfYULpR/ahSB98JP8xaxfikuZwwtT6I/SAo7/Y=.sha256","commonmark-hashtag.js":"&fudY0YdvcMjVCSZ0oiCqUt0+bVT0a06j5TcjWaCDO8E=.sha256"}}
|
File diff suppressed because it is too large
Load Diff
132
apps/cory/ssb/lit-all.min.js
vendored
132
apps/cory/ssb/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
@ -1,57 +0,0 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfConnectionsElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
broadcasts: {type: Array},
|
||||
identities: {type: Array},
|
||||
connections: {type: Array},
|
||||
users: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.broadcasts = [];
|
||||
this.identities = [];
|
||||
this.connections = [];
|
||||
this.users = {};
|
||||
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||
self.identities = identities || [];
|
||||
});
|
||||
}
|
||||
|
||||
_emit_change() {
|
||||
let changed_event = new Event('change', {
|
||||
srcElement: this,
|
||||
});
|
||||
this.dispatchEvent(changed_event);
|
||||
}
|
||||
|
||||
changed(event) {
|
||||
this.selected = event.srcElement.value;
|
||||
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||
this._emit_change();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h2>Broadcasts</h2>
|
||||
<ul>
|
||||
${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
<h2>Connections</h2>
|
||||
<ul>
|
||||
${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
<h2>Local Accounts</h2>
|
||||
<ul>
|
||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-connections', TfConnectionsElement);
|
@ -1,48 +0,0 @@
|
||||
import * as linkify from './commonmark-linkify.js';
|
||||
import * as hashtagify from './commonmark-hashtag.js';
|
||||
|
||||
export function markdown(md) {
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer();
|
||||
var parsed = reader.parse(md || '');
|
||||
parsed = linkify.transform(parsed);
|
||||
parsed = hashtagify.transform(parsed);
|
||||
var walker = parsed.walker();
|
||||
var event, node;
|
||||
while ((event = walker.next())) {
|
||||
node = event.node;
|
||||
if (event.entering) {
|
||||
if (node.type == 'link') {
|
||||
if (node.destination.startsWith('@') &&
|
||||
node.destination.endsWith('.ed25519')) {
|
||||
node.destination = '#' + node.destination;
|
||||
} else if (node.destination.startsWith('%') &&
|
||||
node.destination.endsWith('.sha256')) {
|
||||
node.destination = '#' + node.destination;
|
||||
} else if (node.destination.startsWith('&') &&
|
||||
node.destination.endsWith('.sha256')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
} else if (node.type == 'image') {
|
||||
if (node.destination.startsWith('&')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
export function human_readable_size(bytes) {
|
||||
let v = bytes;
|
||||
let u = 'B';
|
||||
for (let unit of ['kB', 'MB', 'GB']) {
|
||||
if (v > 1024) {
|
||||
v /= 1024;
|
||||
u = unit;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `${Math.round(v * 10) / 10} ${u}`;
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"type":"tildefriends-app","files":{"app.js":"&QUR1tKa15B5Or8AfPX/8Zs87teSeX0Mh/HF7PEPBom0=.sha256","index.html":"&QXhwvxhHc9fa8iL6088hGDu9FgWdY7wkXgvU2BMNv0A=.sha256","lit-core.min.js":"&tP9KhbgwF1chFqPtkNZ12Yx9AfkpnSjFiPcX5Pw5J9g=.sha256","script.js":"&KgOaUVjBM4MzSy7PpUVQHETuvgXAx2JGPJABksBg+QY=.sha256"}}
|
4
apps/db.json
Normal file
4
apps/db.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "💽"
|
||||
}
|
@ -20,7 +20,7 @@ async function database_list() {
|
||||
}
|
||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||
</script>
|
||||
</html>`
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ async function key_list(db) {
|
||||
}
|
||||
populate_dbs('keys', ${JSON.stringify(object)});
|
||||
</script>
|
||||
</html>`
|
||||
</html>`;
|
||||
app.setDocument(doc);
|
||||
}
|
||||
|
4
apps/docs.json
Normal file
4
apps/docs.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "📚"
|
||||
}
|
@ -16,9 +16,7 @@
|
||||
- / => Something good.
|
||||
- update docs
|
||||
- audit + document API exposed to apps
|
||||
- sqlStream => sqlExec or something
|
||||
- fix weird HTTP warnings
|
||||
- ssb from child process?
|
||||
- channels
|
||||
- placeholder/missing images
|
||||
- no denial of service
|
||||
@ -57,7 +55,9 @@
|
||||
- keep working on good error feedback
|
||||
- build for windows
|
||||
- installable apps (bring back an app message?)
|
||||
- sqlStream => sqlExec or something
|
||||
- !ssb from child process?
|
||||
|
||||
## Done
|
||||
- update LICENSE
|
||||
- logging to browser
|
||||
- logging to browser
|
4
apps/follow.json
Normal file
4
apps/follow.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "➡️"
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
var g_following_cache = {};
|
||||
var g_following_deep_cache = {};
|
||||
var g_about_cache = {};
|
||||
@ -15,7 +13,7 @@ async function following(db, id) {
|
||||
f = {users: [], sequence: 0, version: k_version};
|
||||
}
|
||||
f.users = new Set(f.users);
|
||||
await ssb.sqlStream(
|
||||
await ssb.sqlAsync(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" json_extract(content, '$.contact') AS contact, "+
|
||||
@ -73,7 +71,7 @@ async function getAbout(db, id) {
|
||||
if (!f || f.version != k_version) {
|
||||
f = {about: {}, sequence: 0, version: k_version};
|
||||
}
|
||||
await ssb.sqlStream(
|
||||
await ssb.sqlAsync(
|
||||
"SELECT "+
|
||||
" sequence, "+
|
||||
" content "+
|
||||
@ -109,7 +107,7 @@ async function getAbout(db, id) {
|
||||
|
||||
async function getSize(db, id) {
|
||||
let size = 0;
|
||||
await ssb.sqlStream(
|
||||
await ssb.sqlAsync(
|
||||
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||
[id],
|
||||
function (row) {
|
4
apps/sneaker.json
Normal file
4
apps/sneaker.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "👟"
|
||||
}
|
30
apps/sneaker/app.js
Normal file
30
apps/sneaker/app.js
Normal file
@ -0,0 +1,30 @@
|
||||
import * as tfrpc from '/tfrpc.js';
|
||||
|
||||
tfrpc.register(async function getAllIdentities() {
|
||||
return ssb.getAllIdentities();
|
||||
});
|
||||
tfrpc.register(async function query(sql, args) {
|
||||
let result = [];
|
||||
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||
result.push(row);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
tfrpc.register(async function store_blob(blob) {
|
||||
if (Array.isArray(blob)) {
|
||||
blob = Uint8Array.from(blob);
|
||||
}
|
||||
return await ssb.blobStore(blob);
|
||||
});
|
||||
tfrpc.register(async function get_blob(id) {
|
||||
return Array.from(new Uint8Array(await ssb.blobGet(id)));
|
||||
});
|
||||
tfrpc.register(async function store_message(message) {
|
||||
return await ssb.storeMessage(message);
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||
}
|
||||
main();
|
3
apps/sneaker/filesaver.min.js
vendored
Normal file
3
apps/sneaker/filesaver.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
14
apps/sneaker/index.html
Normal file
14
apps/sneaker/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html style="color: #fff">
|
||||
<head>
|
||||
<title>Tilde Friends</title>
|
||||
<base target="_top">
|
||||
</head>
|
||||
<body>
|
||||
<tf-sneaker-app/>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script src="filesaver.min.js"></script>
|
||||
<script src="jszip.min.js"></script>
|
||||
<script src="script.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
13
apps/sneaker/jszip.min.js
vendored
Normal file
13
apps/sneaker/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
126
apps/sneaker/lit-all.min.js
vendored
Normal file
126
apps/sneaker/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/sneaker/lit-all.min.js.map
Normal file
1
apps/sneaker/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
226
apps/sneaker/script.js
Normal file
226
apps/sneaker/script.js
Normal file
@ -0,0 +1,226 @@
|
||||
import {LitElement, html} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
|
||||
class TfSneakerAppElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
feeds: {type: Object},
|
||||
progress: {type: Object},
|
||||
result: {type: String},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.feeds = [];
|
||||
this.progress = undefined;
|
||||
this.result = undefined;
|
||||
}
|
||||
|
||||
async search() {
|
||||
let q = this.renderRoot.getElementById('search').value;
|
||||
let result = await tfrpc.rpc.query(`
|
||||
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||
FROM messages_fts(?)
|
||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||
WHERE
|
||||
json_extract(messages.content, '$.type') = 'about' AND
|
||||
json_extract(messages.content, '$.about') = messages.author AND
|
||||
json_extract(messages.content, '$.name') IS NOT NULL
|
||||
GROUP BY messages.author
|
||||
HAVING MAX(messages.sequence)
|
||||
ORDER BY COUNT(*) DESC
|
||||
`,
|
||||
[`"${q.replaceAll('"', '""')}"`]);
|
||||
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
|
||||
}
|
||||
|
||||
format_message(message) {
|
||||
let out = {
|
||||
previous: message.previous ?? null,
|
||||
};
|
||||
if (message.sequence_before_author) {
|
||||
out.sequence = message.sequence;
|
||||
out.author = message.author;
|
||||
} else {
|
||||
out.author = message.author;
|
||||
out.sequence = message.sequence;
|
||||
}
|
||||
out.timestamp = message.timestamp;
|
||||
out.hash = message.hash;
|
||||
out.content = JSON.parse(message.content);
|
||||
out.signature = message.signature;
|
||||
return {key: message.id, value: out};
|
||||
}
|
||||
|
||||
sanitize(value) {
|
||||
return value.replaceAll('/', '_').replaceAll('+', '-');
|
||||
}
|
||||
|
||||
guess_ext(data) {
|
||||
function startsWith(prefix) {
|
||||
if (data.length < prefix.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < prefix.length; i++) {
|
||||
if (prefix[i] !== null && data[i] !== prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
||||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
||||
return '.jpg';
|
||||
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
||||
return '.png';
|
||||
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
||||
return '.gif';
|
||||
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
||||
return '.webp';
|
||||
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||
return '.svg';
|
||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||
return '.mp3';
|
||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
||||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||
return '.mp4';
|
||||
} else {
|
||||
return '.bin';
|
||||
}
|
||||
}
|
||||
|
||||
async export(id) {
|
||||
let all_messages = '';
|
||||
let sequence = -1;
|
||||
let messages_done = 0;
|
||||
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
||||
while (true) {
|
||||
let messages = await tfrpc.rpc.query(
|
||||
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
||||
[id, sequence]
|
||||
);
|
||||
if (messages?.length) {
|
||||
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
||||
sequence = messages[messages.length - 1].sequence;
|
||||
messages_done += messages.length;
|
||||
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let zip = new JSZip();
|
||||
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
|
||||
|
||||
let blobs = await tfrpc.rpc.query(
|
||||
`SELECT blobs.id
|
||||
FROM messages
|
||||
JOIN messages_refs ON messages.id = messages_refs.message
|
||||
JOIN blobs ON messages_refs.ref = blobs.id
|
||||
WHERE messages.author = ?`,
|
||||
[id]);
|
||||
let blobs_done = 0;
|
||||
for (let row of blobs) {
|
||||
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||
let blob = await tfrpc.rpc.get_blob(row.id);
|
||||
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
||||
blobs_done++;
|
||||
}
|
||||
|
||||
this.progress = {name: 'saving'};
|
||||
let blob = await zip.generateAsync({type: 'blob'});
|
||||
saveAs(blob, `${this.sanitize(id)}.zip`);
|
||||
this.progress = null;
|
||||
}
|
||||
|
||||
keypress(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
async import(event) {
|
||||
let file = event.target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.progress = {name: 'loading'};
|
||||
let zip = new JSZip();
|
||||
file = await zip.loadAsync(file);
|
||||
let messages = [];
|
||||
let blobs = [];
|
||||
file.forEach(function(path, entry) {
|
||||
if (!entry.dir) {
|
||||
if (path.startsWith('message/classic/')) {
|
||||
messages.push(entry);
|
||||
} else {
|
||||
blobs.push(entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
let success = {messages: 0, blobs: 0};
|
||||
let progress = 0;
|
||||
let total_messages = 0;
|
||||
for (let entry of messages) {
|
||||
let lines = (await entry.async('string')).split('\n');
|
||||
total_messages += lines.length;
|
||||
for (let line of lines) {
|
||||
if (!line.length) {
|
||||
continue;
|
||||
}
|
||||
let message = JSON.parse(line);
|
||||
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
||||
if (await tfrpc.rpc.store_message(message.value)) {
|
||||
success.messages++;
|
||||
}
|
||||
}
|
||||
}
|
||||
progress = 0;
|
||||
for (let blob of blobs) {
|
||||
this.progress = {name: 'blobs', value: progress++, max: blobs.length};
|
||||
if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
|
||||
success.blobs++;
|
||||
}
|
||||
}
|
||||
this.progress = undefined;
|
||||
this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
|
||||
}
|
||||
|
||||
render() {
|
||||
let progress;
|
||||
if (this.progress) {
|
||||
if (this.progress.max) {
|
||||
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
|
||||
} else {
|
||||
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||
}
|
||||
}
|
||||
return html`<h1>SSB 👟net</h1>
|
||||
<code>${this.result}</code>
|
||||
${progress}
|
||||
|
||||
<h2>Import</h2>
|
||||
<input type="file" id="import" @change=${this.import}></input>
|
||||
|
||||
<h2>Export</h2>
|
||||
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||
<ul>
|
||||
${Object.entries(this.feeds).map(([id, name]) => html`
|
||||
<li>
|
||||
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||
${name}
|
||||
<code style="color: #ccc">${id}</code>
|
||||
</li>
|
||||
`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-sneaker-app', TfSneakerAppElement);
|
4
apps/ssb.json
Normal file
4
apps/ssb.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "🐌"
|
||||
}
|
@ -47,7 +47,7 @@ tfrpc.register(async function closeConnection(id) {
|
||||
});
|
||||
tfrpc.register(async function query(sql, args) {
|
||||
let result = [];
|
||||
await ssb.sqlStream(sql, args, function callback(row) {
|
||||
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||
result.push(row);
|
||||
});
|
||||
return result;
|
||||
@ -79,9 +79,15 @@ tfrpc.register(async function store_blob(blob) {
|
||||
tfrpc.register(async function get_blob(id) {
|
||||
return utf8Decode(await ssb.blobGet(id));
|
||||
});
|
||||
tfrpc.register(async function store_message(message) {
|
||||
return await ssb.storeMessage(message);
|
||||
});
|
||||
tfrpc.register(function apps() {
|
||||
return core.apps();
|
||||
});
|
||||
tfrpc.register(async function try_decrypt(id, content) {
|
||||
return await ssb.privateMessageDecrypt(id, content);
|
||||
});
|
||||
ssb.addEventListener('broadcasts', async function() {
|
||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||
});
|
@ -39,7 +39,7 @@ function splitMatches(text, regexp) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const regex = new RegExp("\\W#[\\w-]+");
|
||||
const regex = new RegExp("(?<!\w)#[\\w-]+");
|
||||
|
||||
function split(textNodes) {
|
||||
const text = textNodes.map(n => n.literal).join("");
|
@ -54,21 +54,27 @@ export function picker(callback, anchor) {
|
||||
}
|
||||
}
|
||||
|
||||
function chosen(event) {
|
||||
console.log(event.srcElement.innerText);
|
||||
callback(event.srcElement.innerText);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
while (list.firstChild) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
let search = input.value;
|
||||
let any_at_all = false;
|
||||
Object.entries(json).forEach(function(row) {
|
||||
for (let row of Object.entries(json)) {
|
||||
let header = document.createElement('div');
|
||||
header.appendChild(document.createTextNode(row[0]));
|
||||
list.appendChild(header);
|
||||
let any = false;
|
||||
for (let entry of row[1]) {
|
||||
for (let entry of Object.entries(row[1])) {
|
||||
if (search &&
|
||||
search.length &&
|
||||
entry.name.indexOf(search) == -1) {
|
||||
entry[0].indexOf(search) == -1) {
|
||||
continue;
|
||||
}
|
||||
let emoji = document.createElement('span');
|
||||
@ -76,12 +82,9 @@ export function picker(callback, anchor) {
|
||||
emoji.style.display = 'inline-block';
|
||||
emoji.style.overflow = 'hidden';
|
||||
emoji.style.cursor = 'pointer';
|
||||
emoji.onclick = function() {
|
||||
callback(entry);
|
||||
cleanup();
|
||||
}
|
||||
emoji.title = entry.name;
|
||||
emoji.appendChild(document.createTextNode(entry.emoji));
|
||||
emoji.onclick = chosen;
|
||||
emoji.title = entry[0];
|
||||
emoji.appendChild(document.createTextNode(entry[1]));
|
||||
list.appendChild(emoji);
|
||||
any = true;
|
||||
any_at_all = true;
|
||||
@ -89,7 +92,7 @@ export function picker(callback, anchor) {
|
||||
if (!any) {
|
||||
list.removeChild(header);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!any_at_all) {
|
||||
list.appendChild(document.createTextNode('No matches found.'));
|
||||
}
|
1
apps/ssb/emojis.json
Normal file
1
apps/ssb/emojis.json
Normal file
File diff suppressed because one or more lines are too long
3
apps/ssb/filesaver.min.js
vendored
Normal file
3
apps/ssb/filesaver.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||
|
||||
//# sourceMappingURL=FileSaver.min.js.map
|
@ -13,6 +13,7 @@
|
||||
<body>
|
||||
<tf-app/>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script src="filesaver.min.js"></script>
|
||||
<script src="commonmark.min.js"></script>
|
||||
<script src="commonmark-linkify.js" type="module"></script>
|
||||
<script src="commonmark-hashtag.js" type="module"></script>
|
126
apps/ssb/lit-all.min.js
vendored
Normal file
126
apps/ssb/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/ssb/lit-all.min.js.map
Normal file
1
apps/ssb/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -9,5 +9,6 @@ import * as tf_compose from './tf-compose.js';
|
||||
import * as tf_news from './tf-news.js';
|
||||
import * as tf_profile from './tf-profile.js';
|
||||
import * as tf_tab_news from './tf-tab-news.js';
|
||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||
import * as tf_tab_search from './tf-tab-search.js';
|
||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
@ -32,8 +32,8 @@ class TfElement extends LitElement {
|
||||
this.following = [];
|
||||
this.users = {};
|
||||
this.loaded = false;
|
||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
|
||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
|
||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
||||
tfrpc.register(function hashChanged(hash) {
|
||||
self.set_hash(hash);
|
||||
@ -79,7 +79,7 @@ class TfElement extends LitElement {
|
||||
WHERE author = ? AND
|
||||
rowid > ? AND
|
||||
rowid <= ? AND
|
||||
json_extract(content, "$.type") = "contact"
|
||||
json_extract(content, '$.type') = 'contact'
|
||||
ORDER BY sequence
|
||||
`,
|
||||
[id, last_row_id, max_row_id]);
|
||||
@ -133,7 +133,11 @@ class TfElement extends LitElement {
|
||||
`, []))[0].max_row_id;
|
||||
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
||||
cache.last_row_id = max_row_id;
|
||||
await tfrpc.rpc.databaseSet('following', JSON.stringify(cache));
|
||||
let store = JSON.stringify(cache);
|
||||
/* 2023-02-20: Exceeding message size. */
|
||||
//if (store.length < 512 * 1024) {
|
||||
await tfrpc.rpc.databaseSet('following', store);
|
||||
//}
|
||||
return [result, cache.following];
|
||||
}
|
||||
|
||||
@ -276,16 +280,6 @@ class TfElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
add_fake_news() {
|
||||
this.unread = [{
|
||||
author: this.whoami,
|
||||
placeholder: true,
|
||||
id: '%fake_id',
|
||||
text: 'text',
|
||||
content: 'hello',
|
||||
}, ...this.unread];
|
||||
}
|
||||
|
||||
async set_tab(tab) {
|
||||
this.tab = tab;
|
||||
if (tab === 'news') {
|
||||
@ -322,7 +316,6 @@ class TfElement extends LitElement {
|
||||
return html`
|
||||
${this.render_id_picker()}
|
||||
${tabs}
|
||||
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
||||
${contents}
|
||||
`;
|
||||
}
|
@ -11,10 +11,9 @@ class TfComposeElement extends LitElement {
|
||||
users: {type: Object},
|
||||
root: {type: String},
|
||||
branch: {type: String},
|
||||
mentions: {type: Object},
|
||||
apps: {type: Object},
|
||||
drafts: {type: Object},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -24,7 +23,6 @@ class TfComposeElement extends LitElement {
|
||||
this.users = {};
|
||||
this.root = undefined;
|
||||
this.branch = undefined;
|
||||
this.mentions = {};
|
||||
this.apps = undefined;
|
||||
this.drafts = {};
|
||||
}
|
||||
@ -34,6 +32,8 @@ class TfComposeElement extends LitElement {
|
||||
return '';
|
||||
}
|
||||
/* Update mentions. */
|
||||
let draft = this.get_draft();
|
||||
let updated = false;
|
||||
for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
|
||||
let name = match[1];
|
||||
let link = match[2];
|
||||
@ -50,14 +50,19 @@ class TfComposeElement extends LitElement {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.mentions[link]) {
|
||||
this.mentions[link] = {
|
||||
link: link,
|
||||
}
|
||||
if (!draft.mentions) {
|
||||
draft.mentions = {};
|
||||
}
|
||||
this.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||
this.mentions = Object.assign({}, this.mentions);
|
||||
console.log(this.mentions);
|
||||
if (!draft.mentions[link]) {
|
||||
draft.mentions[link] = {
|
||||
link: link,
|
||||
};
|
||||
}
|
||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||
updated = true;
|
||||
}
|
||||
if (updated) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
return tfutils.markdown(text);
|
||||
}
|
||||
@ -66,6 +71,11 @@ class TfComposeElement extends LitElement {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = this.process_text(edit.value);
|
||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
||||
if (content_warning && content_warning_preview) {
|
||||
content_warning_preview.innerText = content_warning.value;
|
||||
}
|
||||
}
|
||||
|
||||
notify(draft) {
|
||||
@ -79,9 +89,11 @@ class TfComposeElement extends LitElement {
|
||||
}));
|
||||
}
|
||||
|
||||
change(event) {
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
this.notify(edit.value);
|
||||
change() {
|
||||
let draft = this.get_draft();
|
||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
||||
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
||||
this.notify(draft);
|
||||
}
|
||||
|
||||
convert_to_format(buffer, type, mime_type) {
|
||||
@ -99,7 +111,7 @@ class TfComposeElement extends LitElement {
|
||||
let data_url = canvas.toDataURL(mime_type);
|
||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
img.onerror = function(event) {
|
||||
reject(new Error('Failed to load image.'));
|
||||
};
|
||||
@ -111,6 +123,7 @@ class TfComposeElement extends LitElement {
|
||||
|
||||
async add_file(file) {
|
||||
try {
|
||||
let draft = this.get_draft();
|
||||
let self = this;
|
||||
let buffer = await file.arrayBuffer();
|
||||
let type = file.type;
|
||||
@ -131,16 +144,19 @@ class TfComposeElement extends LitElement {
|
||||
}
|
||||
let id = await tfrpc.rpc.store_blob(buffer);
|
||||
let name = type.split('/')[0] + ':' + file.name;
|
||||
self.mentions[id] = {
|
||||
if (!draft.mentions) {
|
||||
draft.mentions = {};
|
||||
}
|
||||
draft.mentions[id] = {
|
||||
link: id,
|
||||
name: name,
|
||||
type: type,
|
||||
size: buffer.length ?? buffer.byteLength,
|
||||
};
|
||||
self.mentions = Object.assign({}, self.mentions);
|
||||
let edit = self.renderRoot.getElementById('edit');
|
||||
edit.value += `\n`;
|
||||
self.change();
|
||||
self.input();
|
||||
} catch(e) {
|
||||
alert(e?.message);
|
||||
}
|
||||
@ -162,6 +178,7 @@ class TfComposeElement extends LitElement {
|
||||
|
||||
submit() {
|
||||
let self = this;
|
||||
let draft = this.get_draft();
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
let message = {
|
||||
type: 'post',
|
||||
@ -171,15 +188,18 @@ class TfComposeElement extends LitElement {
|
||||
message.root = this.root;
|
||||
message.branch = this.branch;
|
||||
}
|
||||
if (Object.values(this.mentions).length) {
|
||||
message.mentions = Object.values(this.mentions);
|
||||
if (Object.values(draft.mentions || {}).length) {
|
||||
message.mentions = Object.values(draft.mentions);
|
||||
}
|
||||
if (draft.content_warning !== undefined) {
|
||||
message.contentWarning = draft.content_warning;
|
||||
}
|
||||
console.log('Would post:', message);
|
||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||
edit.value = '';
|
||||
self.mentions = {};
|
||||
self.change();
|
||||
self.notify(undefined);
|
||||
self.requestUpdate();
|
||||
}).catch(function(error) {
|
||||
alert(error.message);
|
||||
});
|
||||
@ -216,17 +236,38 @@ class TfComposeElement extends LitElement {
|
||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||
}
|
||||
|
||||
updated() {
|
||||
super.updated();
|
||||
let edit = this.renderRoot.getElementById('edit');
|
||||
if (this.last_updated_text !== edit.value) {
|
||||
let preview = this.renderRoot.getElementById('preview');
|
||||
preview.innerHTML = this.process_text(edit.value);
|
||||
this.last_updated_text = edit.value;
|
||||
}
|
||||
}
|
||||
|
||||
remove_mention(id) {
|
||||
delete this.mentions[id];
|
||||
this.mentions = Object.assign({}, this.mentions);
|
||||
let draft = this.get_draft();
|
||||
delete draft.mentions[id];
|
||||
this.notify(draft);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render_mention(mention) {
|
||||
let self = this;
|
||||
return html`
|
||||
<div>
|
||||
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
|
||||
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<div style="align-self: center; margin: 0.5em">
|
||||
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column">
|
||||
<h3>${mention.name}</h3>
|
||||
<div style="padding-left: 1em">
|
||||
${Object.entries(mention)
|
||||
.filter(x => x[0] != 'name')
|
||||
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -251,8 +292,11 @@ class TfComposeElement extends LitElement {
|
||||
};
|
||||
}
|
||||
}
|
||||
this.mentions = Object.assign(this.mentions || {}, mentions);
|
||||
this.apps = null;
|
||||
let draft = self.get_draft();
|
||||
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
||||
self.requestUpdate();
|
||||
self.notify(draft);
|
||||
self.apps = null;
|
||||
}
|
||||
|
||||
if (this.apps) {
|
||||
@ -269,24 +313,64 @@ class TfComposeElement extends LitElement {
|
||||
}
|
||||
|
||||
render_attach_app_button() {
|
||||
let self = this;
|
||||
async function attach_app() {
|
||||
this.apps = await tfrpc.rpc.apps();
|
||||
self.apps = await tfrpc.rpc.apps();
|
||||
}
|
||||
if (!this.apps) {
|
||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
|
||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
|
||||
} else {
|
||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
|
||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
|
||||
}
|
||||
}
|
||||
|
||||
set_content_warning(value) {
|
||||
let draft = this.get_draft();
|
||||
draft.content_warning = value;
|
||||
this.notify(draft);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render_content_warning() {
|
||||
let self = this;
|
||||
let draft = this.get_draft();
|
||||
if (draft.content_warning !== undefined) {
|
||||
return html`
|
||||
<div>
|
||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
|
||||
<label for="cw">CW</label>
|
||||
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||
<label for="cw">CW</label>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
get_draft() {
|
||||
return this.drafts[this.branch || ''] || {};
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
let draft = self.get_draft();
|
||||
let content_warning =
|
||||
draft.content_warning !== undefined ?
|
||||
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
||||
undefined;
|
||||
let result = html`
|
||||
<div style="display: flex; flex-direction: row; width: 100%">
|
||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${this.drafts[this.branch || '']}</textarea>
|
||||
<div id="preview" style="flex: 1 0 50%"></div>
|
||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
||||
<div style="flex: 1 0 50%">
|
||||
${content_warning}
|
||||
<div id="preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
${Object.values(this.mentions).map(x => self.render_mention(x))}
|
||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
||||
${this.render_content_warning()}
|
||||
${this.render_attach_app()}
|
||||
<input type="button" value="Submit" @click=${this.submit}></input>
|
||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
@ -9,7 +9,7 @@ class TfIdentityPickerElement extends LitElement {
|
||||
return {
|
||||
ids: {type: Array},
|
||||
selected: {type: String},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
@ -14,7 +14,8 @@ class TfMessageElement extends LitElement {
|
||||
raw: {type: Boolean},
|
||||
blog_data: {type: String},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
decrypted: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -28,6 +29,7 @@ class TfMessageElement extends LitElement {
|
||||
this.drafts = {};
|
||||
this.raw = false;
|
||||
this.expanded = {};
|
||||
this.decrypted = undefined;
|
||||
}
|
||||
|
||||
show_reply() {
|
||||
@ -69,12 +71,12 @@ class TfMessageElement extends LitElement {
|
||||
hash: this.message?.hash,
|
||||
content: this.message?.content,
|
||||
signature: this.message?.signature,
|
||||
}
|
||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
|
||||
};
|
||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
||||
}
|
||||
|
||||
vote(emoji) {
|
||||
let reaction = emoji.emoji;
|
||||
let reaction = emoji;
|
||||
let message = this.message.id;
|
||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
||||
tfrpc.rpc.appendMessage(
|
||||
@ -127,6 +129,13 @@ class TfMessageElement extends LitElement {
|
||||
body_click(event) {
|
||||
if (event.srcElement.tagName == 'IMG') {
|
||||
this.show_image(event.srcElement.src);
|
||||
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
||||
let next = event.srcElement.nextSibling;
|
||||
if (next.style.display == 'block') {
|
||||
next.style.display = 'none';
|
||||
} else {
|
||||
next.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +157,7 @@ class TfMessageElement extends LitElement {
|
||||
} else if (mention.link?.startsWith('&') &&
|
||||
mention.name?.startsWith('video:')) {
|
||||
return html`
|
||||
<video controls style="max-height: 240px">
|
||||
<video controls style="max-height: 240px; max-width: 128px">
|
||||
<source src=${'/' + mention.link + '/view'}></source>
|
||||
</video>
|
||||
`;
|
||||
@ -168,10 +177,7 @@ class TfMessageElement extends LitElement {
|
||||
|
||||
render_mentions() {
|
||||
let mentions = this.message?.content?.mentions || [];
|
||||
mentions = mentions.filter(x =>
|
||||
x.name?.startsWith('audio:') ||
|
||||
x.name?.startsWith('video:') ||
|
||||
this.message?.content?.text?.indexOf(x.link) === -1);
|
||||
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
||||
if (mentions.length) {
|
||||
let self = this;
|
||||
return html`
|
||||
@ -214,24 +220,43 @@ class TfMessageElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
async try_decrypt(content) {
|
||||
let result = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||
if (result) {
|
||||
this.decrypted = JSON.parse(result);
|
||||
} else {
|
||||
this.decrypted = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let content = this.message?.content;
|
||||
if (this.decrypted?.type == 'post') {
|
||||
content = this.decrypted;
|
||||
}
|
||||
let self = this;
|
||||
let raw_button = this.raw ?
|
||||
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
||||
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
||||
function small_frame(inner) {
|
||||
return html`
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
||||
${raw_button}
|
||||
${self.raw ? self.render_raw() : inner}
|
||||
${self.render_votes()}
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
if (this.message.placeholder) {
|
||||
if (this.message?.type === 'contact_group') {
|
||||
return html`
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||
${this.message.messages.map(x =>
|
||||
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
||||
)}
|
||||
</div>`;
|
||||
} else if (this.message.placeholder) {
|
||||
return html`
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
||||
@ -258,7 +283,7 @@ class TfMessageElement extends LitElement {
|
||||
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||
</div>
|
||||
`
|
||||
`;
|
||||
}
|
||||
let update = content.about == this.message.author ?
|
||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
||||
@ -270,8 +295,9 @@ class TfMessageElement extends LitElement {
|
||||
${description}
|
||||
`);
|
||||
} else if (content.type == 'contact') {
|
||||
return small_frame(html`
|
||||
return html`
|
||||
<div>
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
is
|
||||
${
|
||||
content.blocking === true ? 'blocking' :
|
||||
@ -282,7 +308,7 @@ class TfMessageElement extends LitElement {
|
||||
}
|
||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
||||
</div>
|
||||
`);
|
||||
`;
|
||||
} else if (content.type == 'post') {
|
||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
||||
<tf-compose
|
||||
@ -300,7 +326,7 @@ class TfMessageElement extends LitElement {
|
||||
this.render_raw() :
|
||||
unsafeHTML(tfutils.markdown(content.text));
|
||||
let content_warning = html`
|
||||
<div style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
||||
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
||||
`;
|
||||
let content_html =
|
||||
html`
|
||||
@ -316,6 +342,8 @@ class TfMessageElement extends LitElement {
|
||||
` :
|
||||
content_warning :
|
||||
content_html;
|
||||
let is_encrypted = this.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||
let style_background = this.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||
return html`
|
||||
<style>
|
||||
code {
|
||||
@ -331,9 +359,10 @@ class TfMessageElement extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
||||
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||
<div style="display: flex; flex-direction: row">
|
||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||
${is_encrypted}
|
||||
<span style="flex: 1"></span>
|
||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||
<span>${raw_button}</span>
|
||||
@ -400,6 +429,11 @@ class TfMessageElement extends LitElement {
|
||||
`;
|
||||
} else if (content.type === 'pub') {
|
||||
return small_frame(html`
|
||||
<style>
|
||||
span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
<span>
|
||||
<div>
|
||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
||||
@ -413,7 +447,14 @@ class TfMessageElement extends LitElement {
|
||||
</div>
|
||||
`);
|
||||
} else if (typeof(this.message.content) == 'string') {
|
||||
return small_frame(html`<span>🔒</span>`);
|
||||
if (this.decrypted) {
|
||||
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
|
||||
} else if (this.decrypted === undefined) {
|
||||
this.try_decrypt(content);
|
||||
return small_frame(html`<span>🔐</span>`);
|
||||
} else {
|
||||
return small_frame(html`<span>🔒</span>`);
|
||||
}
|
||||
} else {
|
||||
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||
}
|
@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
|
||||
following: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -145,9 +145,29 @@ class TfNewsElement extends LitElement {
|
||||
return recursive_sort(roots, true);
|
||||
}
|
||||
|
||||
async load_and_render(messages) {
|
||||
group_following(messages) {
|
||||
let result = [];
|
||||
let group = [];
|
||||
for (let message of messages) {
|
||||
if (message?.content?.type === 'contact') {
|
||||
group.push(message);
|
||||
} else {
|
||||
if (group.length > 0) {
|
||||
result.push({
|
||||
type: 'contact_group',
|
||||
messages: group,
|
||||
});
|
||||
group = [];
|
||||
}
|
||||
result.push(message);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
load_and_render(messages) {
|
||||
let messages_by_id = this.process_messages(messages);
|
||||
let final_messages = this.finalize_messages(messages_by_id);
|
||||
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
||||
return html`
|
||||
<div style="display: flex; flex-direction: column">
|
||||
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
||||
@ -156,8 +176,7 @@ class TfNewsElement extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
let messages = this.load_and_render(this.messages || []);
|
||||
return html`${until(messages, html`<div>Loading placeholders...</div>`)}`;
|
||||
return this.load_and_render(this.messages || []);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ class TfProfileElement extends LitElement {
|
||||
id: {type: String},
|
||||
users: {type: Object},
|
||||
size: {type: Number},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -33,7 +33,7 @@ class TfProfileElement extends LitElement {
|
||||
contact: this.id,
|
||||
}, change)).catch(function(error) {
|
||||
alert(error?.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
follow() {
|
||||
@ -148,6 +148,10 @@ class TfProfileElement extends LitElement {
|
||||
<div><label for="description">Description:</label></div>
|
||||
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
||||
</div>
|
||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
||||
</div>` : null;
|
||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
@ -29,4 +29,20 @@ img {
|
||||
color: #088;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.content_warning {
|
||||
border: 1px solid #fff;
|
||||
border-radius: 1em;
|
||||
padding: 8px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
div.img_caption {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.img_caption::after {
|
||||
content: ' ±';
|
||||
}
|
||||
`;
|
@ -9,7 +9,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
connections: {type: Array},
|
||||
stored_connections: {type: Array},
|
||||
users: {type: Object},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
@ -71,7 +71,7 @@ class TfTabConnectionsElement extends LitElement {
|
||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||
${this.render_connection_summary(connection)}
|
||||
</li>
|
||||
`
|
||||
`;
|
||||
}
|
||||
|
||||
async forget_stored_connection(connection) {
|
154
apps/ssb/tf-tab-news-feed.js
Normal file
154
apps/ssb/tf-tab-news-feed.js
Normal file
@ -0,0 +1,154 @@
|
||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfTabNewsFeedElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
hash: {type: String},
|
||||
following: {type: Array},
|
||||
messages: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.users = {};
|
||||
this.hash = '#';
|
||||
this.following = [];
|
||||
this.drafts = {};
|
||||
this.expanded = {};
|
||||
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
async fetch_messages() {
|
||||
if (this.hash.startsWith('#@')) {
|
||||
let r = await tfrpc.rpc.query(
|
||||
`
|
||||
WITH mine AS (SELECT messages.*
|
||||
FROM messages
|
||||
WHERE messages.author = ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 20)
|
||||
SELECT messages.*
|
||||
FROM mine
|
||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT * FROM mine
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
return r;
|
||||
} else if (this.hash.startsWith('#%')) {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
FROM messages
|
||||
WHERE id = ?1
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM messages JOIN messages_refs
|
||||
ON messages.id = messages_refs.message
|
||||
WHERE messages_refs.ref = ?1
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
} else {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
WITH news AS (SELECT messages.*
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ?
|
||||
ORDER BY messages.timestamp DESC)
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
JOIN messages ON messages_refs.ref = messages.id
|
||||
UNION
|
||||
SELECT news.* FROM news
|
||||
`,
|
||||
[
|
||||
JSON.stringify(this.following),
|
||||
this.start_time,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async load_more() {
|
||||
let last_start_time = this.start_time;
|
||||
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||
let more = await tfrpc.rpc.query(
|
||||
`
|
||||
WITH news AS (SELECT messages.*
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ?
|
||||
AND messages.timestamp <= ?
|
||||
ORDER BY messages.timestamp DESC)
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
JOIN messages ON messages_refs.ref = messages.id
|
||||
UNION
|
||||
SELECT news.* FROM news
|
||||
`,
|
||||
[
|
||||
JSON.stringify(this.following),
|
||||
this.start_time,
|
||||
last_start_time,
|
||||
]);
|
||||
this.messages = [...more, ...this.messages];
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.messages ||
|
||||
this._messages_hash !== this.hash ||
|
||||
this._messages_following !== this.following) {
|
||||
console.log(`loading messages for ${this.whoami}`);
|
||||
let self = this;
|
||||
this.messages = [];
|
||||
this._messages_hash = this.hash;
|
||||
this._messages_following = this.following;
|
||||
this.fetch_messages().then(function(messages) {
|
||||
self.messages = messages;
|
||||
console.log(`loading mesages done for ${self.whoami}`);
|
||||
}).catch(function(error) {
|
||||
alert(JSON.stringify(error, null, 2));
|
||||
});
|
||||
}
|
||||
let more;
|
||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||
more = html`
|
||||
<input type="button" value="Load More" @click=${this.load_more}></input>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
|
||||
${more}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
@ -2,114 +2,6 @@ import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||
import * as tfrpc from '/static/tfrpc.js';
|
||||
import {styles} from './tf-styles.js';
|
||||
|
||||
class TfTabNewsFeedElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
whoami: {type: String},
|
||||
users: {type: Object},
|
||||
hash: {type: String},
|
||||
following: {type: Array},
|
||||
messages: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
let self = this;
|
||||
this.whoami = null;
|
||||
this.users = {};
|
||||
this.hash = '#';
|
||||
this.following = [];
|
||||
this.drafts = {};
|
||||
this.expanded = {};
|
||||
}
|
||||
|
||||
async fetch_messages() {
|
||||
if (this.hash.startsWith('#@')) {
|
||||
let r = await tfrpc.rpc.query(
|
||||
`
|
||||
WITH mine AS (SELECT messages.*
|
||||
FROM messages
|
||||
WHERE messages.author = ?
|
||||
ORDER BY sequence DESC
|
||||
LIMIT 20)
|
||||
SELECT messages.*
|
||||
FROM mine
|
||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT * FROM mine
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
return r;
|
||||
} else if (this.hash.startsWith('#%')) {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
SELECT messages.*
|
||||
FROM messages
|
||||
WHERE id = ?1
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM messages JOIN messages_refs
|
||||
ON messages.id = messages_refs.message
|
||||
WHERE messages_refs.ref = ?1
|
||||
`,
|
||||
[
|
||||
this.hash.substring(1),
|
||||
]);
|
||||
} else {
|
||||
return await tfrpc.rpc.query(
|
||||
`
|
||||
WITH news AS (SELECT messages.*
|
||||
FROM messages
|
||||
JOIN json_each(?) AS following ON messages.author = following.value
|
||||
WHERE messages.timestamp > ?
|
||||
ORDER BY messages.timestamp DESC)
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.ref
|
||||
JOIN messages ON messages_refs.message = messages.id
|
||||
UNION
|
||||
SELECT messages.*
|
||||
FROM news
|
||||
JOIN messages_refs ON news.id = messages_refs.message
|
||||
JOIN messages ON messages_refs.ref = messages.id
|
||||
UNION
|
||||
SELECT news.* FROM news
|
||||
`,
|
||||
[
|
||||
JSON.stringify(this.following),
|
||||
new Date().valueOf() - 24 * 60 * 60 * 1000,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.messages ||
|
||||
this._messages_hash !== this.hash ||
|
||||
this._messages_following !== this.following) {
|
||||
console.log(`loading messages for ${this.whoami}`);
|
||||
let self = this;
|
||||
this.messages = [];
|
||||
this._messages_hash = this.hash;
|
||||
this._messages_following = this.following;
|
||||
this.fetch_messages().then(function(messages) {
|
||||
self.messages = messages;
|
||||
console.log(`loading mesages done for ${self.whoami}`);
|
||||
}).catch(function(error) {
|
||||
alert(JSON.stringify(error, null, 2));
|
||||
});
|
||||
}
|
||||
return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>`;
|
||||
}
|
||||
}
|
||||
|
||||
class TfTabNewsElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
@ -120,7 +12,7 @@ class TfTabNewsElement extends LitElement {
|
||||
following: {type: Array},
|
||||
drafts: {type: Object},
|
||||
expanded: {type: Object},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -141,9 +33,19 @@ class TfTabNewsElement extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.body.addEventListener('keypress', this.on_keypress.bind(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
|
||||
}
|
||||
|
||||
show_more() {
|
||||
let unread = this.unread;
|
||||
let news = this.renderRoot?.getElementById('news');
|
||||
let news = this.shadowRoot?.getElementById('news');
|
||||
if (news) {
|
||||
console.log('injecting messages', news.messages);
|
||||
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
||||
@ -193,6 +95,13 @@ class TfTabNewsElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
on_keypress(event) {
|
||||
if (event.target === document.body &&
|
||||
event.key == '.') {
|
||||
this.show_more();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let profile = this.hash.startsWith('#@') ?
|
||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||
@ -207,5 +116,4 @@ class TfTabNewsElement extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
||||
customElements.define('tf-tab-news', TfTabNewsElement);
|
@ -9,7 +9,7 @@ class TfTabSearchElement extends LitElement {
|
||||
users: {type: Object},
|
||||
following: {type: Array},
|
||||
query: {type: String},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
|
||||
return {
|
||||
id: {type: String},
|
||||
users: {type: Object},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
@ -19,18 +19,23 @@ class TfUserElement extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
let name = this.users?.[this.id]?.name;
|
||||
name = name !== undefined ?
|
||||
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
||||
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||
|
||||
if (this.users[this.id]) {
|
||||
let image = this.users[this.id].image;
|
||||
image = typeof(image) == 'string' ? image : image?.link;
|
||||
return html`
|
||||
<div style="display: inline-block; font-weight: bold">
|
||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
||||
<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a>
|
||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
||||
${name}
|
||||
</div>`;
|
||||
} else {
|
||||
return html`
|
||||
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
|
||||
<a target="_top" href=${'#' + this.id}>${this.id}</a>
|
||||
<div style="display: inline-block; font-weight: bold">
|
||||
${name}
|
||||
</div>`;
|
||||
}
|
||||
}
|
93
apps/ssb/tf-utils.js
Normal file
93
apps/ssb/tf-utils.js
Normal file
@ -0,0 +1,93 @@
|
||||
import * as linkify from './commonmark-linkify.js';
|
||||
import * as hashtagify from './commonmark-hashtag.js';
|
||||
|
||||
function image(node, entering) {
|
||||
if (node.firstChild?.type === 'text' &&
|
||||
node.firstChild.literal.startsWith('video:')) {
|
||||
if (entering) {
|
||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
this.lit('</video>');
|
||||
}
|
||||
} else if (node.firstChild?.type === 'text' &&
|
||||
node.firstChild.literal.startsWith('audio:')) {
|
||||
if (entering) {
|
||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
this.lit('</audio>');
|
||||
}
|
||||
} else {
|
||||
if (entering) {
|
||||
if (this.disableTags === 0) {
|
||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||
this.lit('<img src="" alt="');
|
||||
} else {
|
||||
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
||||
}
|
||||
}
|
||||
this.disableTags += 1;
|
||||
} else {
|
||||
this.disableTags -= 1;
|
||||
if (this.disableTags === 0) {
|
||||
if (node.title) {
|
||||
this.lit('" title="' + this.esc(node.title));
|
||||
}
|
||||
this.lit('" />');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function markdown(md) {
|
||||
var reader = new commonmark.Parser({safe: true});
|
||||
var writer = new commonmark.HtmlRenderer();
|
||||
writer.image = image;
|
||||
var parsed = reader.parse(md || '');
|
||||
parsed = linkify.transform(parsed);
|
||||
parsed = hashtagify.transform(parsed);
|
||||
var walker = parsed.walker();
|
||||
var event, node;
|
||||
while ((event = walker.next())) {
|
||||
node = event.node;
|
||||
if (event.entering) {
|
||||
if (node.type == 'link') {
|
||||
if (node.destination.startsWith('@') &&
|
||||
node.destination.endsWith('.ed25519')) {
|
||||
node.destination = '#' + node.destination;
|
||||
} else if (node.destination.startsWith('%') &&
|
||||
node.destination.endsWith('.sha256')) {
|
||||
node.destination = '#' + node.destination;
|
||||
} else if (node.destination.startsWith('&') &&
|
||||
node.destination.endsWith('.sha256')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
} else if (node.type == 'image') {
|
||||
if (node.destination.startsWith('&')) {
|
||||
node.destination = '/' + node.destination + '/view';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
export function human_readable_size(bytes) {
|
||||
let v = bytes;
|
||||
let u = 'B';
|
||||
for (let unit of ['kB', 'MB', 'GB']) {
|
||||
if (v > 1024) {
|
||||
v /= 1024;
|
||||
u = unit;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return `${Math.round(v * 10) / 10} ${u}`;
|
||||
}
|
4
apps/todo.json
Normal file
4
apps/todo.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "tildefriends-app",
|
||||
"emoji": "☑️"
|
||||
}
|
@ -114,13 +114,12 @@ class TodoListElement extends LitElement {
|
||||
@change=${event => self.input_change(event, item)}
|
||||
@keydown=${event => self.input_keydown(event, item)}
|
||||
@blur=${x => self.input_blur(item)}></input>
|
||||
<span @click=${x => self.remove_item(item)}>x</span></div>
|
||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||
`;
|
||||
} else {
|
||||
return html`
|
||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
||||
<span @click=${x => self.editing = index}>${item.text}</span>
|
||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -175,7 +174,8 @@ class TodoListElement extends LitElement {
|
||||
return html`
|
||||
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
||||
${name}
|
||||
${(this.items || []).map(x => self.render_item(x))}
|
||||
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
|
||||
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
|
||||
<button @click=${self.add_item}>+ Item</button>
|
||||
<button @click=${self.remove_list}>- List</button>
|
||||
</div>
|
@ -1,7 +1,6 @@
|
||||
import * as core from './core.js';
|
||||
import * as form from './form.js';
|
||||
|
||||
let gTokens = {};
|
||||
let gDatabase = new Database("auth");
|
||||
|
||||
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
||||
@ -171,7 +170,7 @@ function handler(request, response) {
|
||||
}
|
||||
}
|
||||
|
||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; Secure; SameSite=Strict`;
|
||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict`;
|
||||
let entry = readSession(session);
|
||||
if (entry && formData.return) {
|
||||
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
||||
@ -225,7 +224,7 @@ function handler(request, response) {
|
||||
});
|
||||
}
|
||||
} else if (request.uri == "/login/logout") {
|
||||
response.writeHead(303, {"Set-Cookie": "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
||||
response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT`, "Location": "/login" + (request.query ? "?" + request.query : "")});
|
||||
response.end();
|
||||
} else {
|
||||
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||
|
545
core/client.js
545
core/client.js
@ -1,10 +1,10 @@
|
||||
import {LitElement, html, css, svg} from '/static/lit/lit-all.min.js';
|
||||
|
||||
let gSocket;
|
||||
let gCredentials;
|
||||
let gPermissions;
|
||||
|
||||
let gCurrentFile;
|
||||
let gFiles = {};
|
||||
let gApp = {files: {}};
|
||||
let gApp = {files: {}, emoji: '📦'};
|
||||
let gEditor;
|
||||
let gSplit;
|
||||
let gGraphs = {};
|
||||
@ -26,6 +26,304 @@ const k_api = {
|
||||
setHash: {args: ['hash'], func: api_setHash},
|
||||
};
|
||||
|
||||
const k_global_style = css`
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #6c71c4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #859900;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #2aa198;
|
||||
}
|
||||
`;
|
||||
|
||||
class TfNavigationElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
credentials: {type: Object},
|
||||
permissions: {type: Object},
|
||||
show_permissions: {type: Boolean},
|
||||
status: {type: Object},
|
||||
spark_lines: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.permissions = {};
|
||||
this.show_permissions = false;
|
||||
this.status = {};
|
||||
this.spark_lines = {};
|
||||
}
|
||||
|
||||
toggle_edit(event) {
|
||||
event.preventDefault();
|
||||
if (editing()) {
|
||||
closeEditor();
|
||||
} else {
|
||||
edit();
|
||||
}
|
||||
}
|
||||
|
||||
reset_permission(key) {
|
||||
send({action: "resetPermission", permission: key});
|
||||
}
|
||||
|
||||
get_spark_line(key, options) {
|
||||
if (!this.spark_lines[key]) {
|
||||
let spark_line = document.createElement('tf-sparkline');
|
||||
spark_line.title = key;
|
||||
if (options) {
|
||||
if (options.max) {
|
||||
spark_line.max = options.max;
|
||||
}
|
||||
}
|
||||
this.spark_lines[key] = spark_line;
|
||||
this.requestUpdate();
|
||||
}
|
||||
return this.spark_lines[key];
|
||||
}
|
||||
|
||||
render_login() {
|
||||
if (this?.credentials?.session?.name) {
|
||||
return html`<a href="/login/logout?return=${url() + hash()}">logout ${this.credentials.session.name}</a>`;
|
||||
} else {
|
||||
return html`<a href="/login?return=${url() + hash()}">login</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
render_permissions() {
|
||||
if (this.show_permissions) {
|
||||
return html`
|
||||
<div style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%">
|
||||
<div style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff">
|
||||
<div>This app has the following permissions:</div>
|
||||
${Object.keys(this.permissions).map(key => html`
|
||||
<div>
|
||||
<span>${key}</span>: ${this.permissions[key] ? '✅ Allowed' : '❌ Denied'}
|
||||
<button @click=${() => this.reset_permission(key)}>Reset</button>
|
||||
</div>
|
||||
`)}
|
||||
<button @click=${() => this.show_permissions = false}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<style>
|
||||
${k_global_style}
|
||||
</style>
|
||||
<div style="margin: 4px; display: flex; flex-direction: row; flex-wrap: nowrap; gap: 3px">
|
||||
<span>😎</span>
|
||||
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff; white-space: nowrap">TF</a>
|
||||
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||
<a accesskey="e" data-tip="Toggle the app editor." href="#" @click=${this.toggle_edit}>edit</a>
|
||||
<a accesskey="p" data-tip="View and change permissions." href="#" @click=${() => self.show_permissions = !self.show_permissions}>🎛️</a>
|
||||
<span style="display: inline-block; vertical-align: top; white-space: pre; color: ${this.status.color ?? kErrorColor}">${this.status.message}</span>
|
||||
<span id="requests"></span>
|
||||
${this.render_permissions()}
|
||||
<span style="flex: 1; white-space: nowrap; overflow: hidden; margin: 0; padding: 0">${Object.keys(this.spark_lines).sort().map(x => this.spark_lines[x]).map(x => [x.dataset.emoji, x])}</span>
|
||||
<span style="flex: 0 0; white-space: nowrap">${this.render_login()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-navigation', TfNavigationElement);
|
||||
|
||||
class TfFilesElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
current: {type: String},
|
||||
files: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.files = {};
|
||||
}
|
||||
|
||||
file_click(file) {
|
||||
this.dispatchEvent(new CustomEvent('file_click', {
|
||||
detail: {
|
||||
file: file,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
render_file(file) {
|
||||
let classes = ['file'];
|
||||
if (file == this.current) {
|
||||
classes.push('current');
|
||||
}
|
||||
if (!this.files[file].clean) {
|
||||
classes.push('dirty');
|
||||
}
|
||||
return html`<div class="${classes.join(' ')}" @click=${x => this.file_click(file)}>${file}</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
return html`
|
||||
<style>
|
||||
div.file {
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
div.file:hover {
|
||||
background-color: #1a9188;
|
||||
}
|
||||
div.file::before {
|
||||
content: '📄 ';
|
||||
}
|
||||
|
||||
div.file.current {
|
||||
font-weight: bold;
|
||||
background-color: #2aa198;
|
||||
}
|
||||
|
||||
div.file.dirty::after {
|
||||
content: '*';
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
${Object.keys(this.files).sort().map(x => self.render_file(x))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-files', TfFilesElement);
|
||||
|
||||
class TfFilesPaneElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
expanded: {type: Boolean},
|
||||
current: {type: String},
|
||||
files: {type: Object},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.expanded = window.localStorage.getItem('files') != '0';
|
||||
this.files = {};
|
||||
}
|
||||
|
||||
set_expanded(expanded) {
|
||||
this.expanded = expanded;
|
||||
window.localStorage.setItem('files', expanded ? '1' : '0');
|
||||
}
|
||||
|
||||
render() {
|
||||
let self = this;
|
||||
let expander = this.expanded ?
|
||||
html`<span @click=${() => self.set_expanded(false)} class="expander">«</span>` :
|
||||
html`<span @click=${() => self.set_expanded(true)} class="expander">»</span>`;
|
||||
let content = html`
|
||||
<div id="files_content">
|
||||
<tf-files .files=${self.files} current=${self.current} @file_click=${event => openFile(event.detail.file)}></tf-files>
|
||||
<br>
|
||||
<div><button @click=${() => newFile()}>New File</button></div>
|
||||
<div><button @click=${() => removeFile()}>Remove File</button></div>
|
||||
</div>
|
||||
`;
|
||||
return html`
|
||||
<style>
|
||||
.expander {
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
right: 0;
|
||||
flex: 0;
|
||||
padding: 0.25em;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div style="display: flex; flex-direction: row">
|
||||
${this.expanded ? html`<span style="font-weight: bold; text-align: center; flex: 1">Files</span>` : undefined}
|
||||
${expander}
|
||||
</div>
|
||||
${this.expanded ? content : undefined}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-files-pane', TfFilesPaneElement);
|
||||
|
||||
class TfSparkLineElement extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
lines: {type: Array},
|
||||
min: {type: Number},
|
||||
max: {type: Number},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.min = 0;
|
||||
this.max = 1.0;
|
||||
this.lines = [];
|
||||
this.k_values_max = 100;
|
||||
}
|
||||
|
||||
append(key, value) {
|
||||
let line = null;
|
||||
for (let it of this.lines) {
|
||||
if (it.name == key) {
|
||||
line = it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!line) {
|
||||
const k_colors = ['#0f0', '#88f', '#ff0', '#f0f', '#0ff', '#f00', '#888'];
|
||||
line = {
|
||||
name: key,
|
||||
style: k_colors[this.lines.length % k_colors.length],
|
||||
values: Array(this.k_values_max).fill(0),
|
||||
};
|
||||
this.lines.push(line);
|
||||
}
|
||||
if (line.values.length >= this.k_values_max) {
|
||||
line.values.shift();
|
||||
}
|
||||
line.values.push(value);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
render_line(line) {
|
||||
if (line?.values?.length >= 2) {
|
||||
let max = Math.max(this.max, ...line.values);
|
||||
let points = [].concat(...line.values.map((x, i) => [100.0 * i / (line.values.length - 1), 10.0 - 10.0 * (x - this.min) / (max - this.min)]));
|
||||
return svg`<polyline points=${points.join(' ')} stroke=${line.style} fill="none"/>`;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let max = Math.round(10.0 * Math.max(...this.lines.map(line => line.values[line.values.length - 1]))) / 10.0;
|
||||
return html`
|
||||
<svg style="width: 10em; height: 1.4em; vertical-align: top; margin: 0; padding: 0; background: #000" viewBox="0 0 100 10" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
${this.lines.map(x => this.render_line(x))}
|
||||
<text x="0" y="1em" style="font: 8px sans-serif; fill: #fff">${max}</text>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('tf-sparkline', TfSparkLineElement);
|
||||
|
||||
window.addEventListener("keydown", function(event) {
|
||||
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
|
||||
if (editing()) {
|
||||
@ -81,14 +379,6 @@ function editing() {
|
||||
return document.getElementById("editPane").style.display != 'none';
|
||||
}
|
||||
|
||||
function toggleEdit() {
|
||||
if (editing()) {
|
||||
closeEditor();
|
||||
} else {
|
||||
edit();
|
||||
}
|
||||
}
|
||||
|
||||
function edit() {
|
||||
if (editing()) {
|
||||
return;
|
||||
@ -100,6 +390,7 @@ function edit() {
|
||||
gSplit = undefined;
|
||||
}
|
||||
gSplit = Split(['#editPane', '#viewPane'], {minSize: 0});
|
||||
document.getElementById("editPane").style.display = 'flex';
|
||||
|
||||
ensureLoaded([
|
||||
{tagName: "script", attributes: {src: "/codemirror/codemirror.min.js"}},
|
||||
@ -107,6 +398,7 @@ function edit() {
|
||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/matchesonscrollbar.min.css"}},
|
||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/dialog.min.css"}},
|
||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/codemirror.min.css"}},
|
||||
{tagName: "link", attributes: {rel: "stylesheet", href: "/codemirror/lint.css"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/trailingspace.min.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/dialog.min.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/search.min.js"}},
|
||||
@ -118,6 +410,9 @@ function edit() {
|
||||
{tagName: "script", attributes: {src: "/codemirror/css.min.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/xml.min.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/htmlmixed.min.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/lint.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/jshint.js"}},
|
||||
{tagName: "script", attributes: {src: "/codemirror/javascript-lint.min.js"}},
|
||||
], function() {
|
||||
load().catch(function(error) {
|
||||
alert(error);
|
||||
@ -126,30 +421,18 @@ function edit() {
|
||||
});
|
||||
}
|
||||
|
||||
function hideFiles() {
|
||||
window.localStorage.setItem('files', '0');
|
||||
document.getElementById('filesPane').classList.add('collapsed');
|
||||
}
|
||||
|
||||
function showFiles() {
|
||||
window.localStorage.setItem('files', '1');
|
||||
document.getElementById('filesPane').classList.remove('collapsed');
|
||||
}
|
||||
|
||||
function trace() {
|
||||
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}&title=Tilde%20Friends`);
|
||||
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
|
||||
}
|
||||
|
||||
function stats() {
|
||||
window.localStorage.setItem('stats', '1');
|
||||
document.getElementById("statsPane").style.display = 'flex';
|
||||
send({action: 'enableStats', enabled: true});
|
||||
}
|
||||
|
||||
function closeStats() {
|
||||
window.localStorage.setItem('stats', '0');
|
||||
document.getElementById("statsPane").style.display = 'none';
|
||||
send({action: 'enableStats', enabled: false});
|
||||
}
|
||||
|
||||
function toggleStats() {
|
||||
@ -175,7 +458,6 @@ function loadFile(name, id) {
|
||||
}).then(function(text) {
|
||||
gFiles[name].doc = new CodeMirror.Doc(text, guessMode(name));
|
||||
if (!Object.values(gFiles).some(x => !x.doc)) {
|
||||
document.getElementById("editPane").style.display = 'flex';
|
||||
openFile(Object.keys(gFiles).sort()[0]);
|
||||
}
|
||||
});
|
||||
@ -200,6 +482,13 @@ function load(path) {
|
||||
'indentUnit': 4,
|
||||
'indentWithTabs': true,
|
||||
'showTrailingSpace': true,
|
||||
'gutters': ['CodeMirror-lint-markers'],
|
||||
'mode': {'js': 'javascript'}[(path || url()).split('.').pop()],
|
||||
'lint': {
|
||||
'options': {
|
||||
'esversion': 2021,
|
||||
},
|
||||
},
|
||||
});
|
||||
gEditor.on('changes', function() {
|
||||
updateFiles();
|
||||
@ -219,6 +508,8 @@ function load(path) {
|
||||
document.getElementById("editPane").style.display = 'flex';
|
||||
}
|
||||
gApp = json;
|
||||
gApp.emoji = gApp.emoji || '📦';
|
||||
document.getElementById('icon').value = gApp.emoji;
|
||||
}
|
||||
if (!isApp) {
|
||||
document.getElementById("editPane").style.display = 'flex';
|
||||
@ -293,6 +584,7 @@ function save(save_to) {
|
||||
let app = {
|
||||
type: "tildefriends-app",
|
||||
files: Object.fromEntries(Object.keys(gFiles).map(x => [x, gFiles[x].id || gApp.files[x]])),
|
||||
emoji: gApp.emoji || '📦',
|
||||
};
|
||||
Object.values(gFiles).forEach(function(file) { delete file.id; });
|
||||
gApp = JSON.parse(JSON.stringify(app));
|
||||
@ -325,6 +617,14 @@ function save(save_to) {
|
||||
});
|
||||
}
|
||||
|
||||
function changeIcon() {
|
||||
let value = prompt('Enter a new app icon emoji:');
|
||||
if (value !== undefined) {
|
||||
gApp.emoji = value || '📦';
|
||||
document.getElementById('icon').value = gApp.emoji;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteApp() {
|
||||
let name = document.getElementById("name");
|
||||
let path = name && name.value ? name.value : url();
|
||||
@ -390,7 +690,8 @@ function api_localStorageGet(key) {
|
||||
}
|
||||
|
||||
function api_requestPermission(permission, id) {
|
||||
let permissions = document.getElementById('permissions');
|
||||
let outer = document.createElement('div');
|
||||
outer.classList.add('permissions');
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.classList.add('permissions_contents');
|
||||
@ -434,17 +735,14 @@ function api_requestPermission(permission, id) {
|
||||
button.innerText = option.text;
|
||||
button.onclick = function() {
|
||||
resolve(option.grant[check.checked ? 1 : 0]);
|
||||
while (permissions.firstChild) {
|
||||
permissions.removeChild(permissions.firstChild);
|
||||
}
|
||||
permissions.style.visibility = 'hidden';
|
||||
document.body.removeChild(outer);
|
||||
}
|
||||
div.appendChild(button);
|
||||
}
|
||||
container.appendChild(div);
|
||||
outer.appendChild(container);
|
||||
|
||||
permissions.appendChild(container);
|
||||
permissions.style.visibility = 'visible';
|
||||
document.body.appendChild(outer);
|
||||
});
|
||||
}
|
||||
|
||||
@ -456,85 +754,18 @@ function api_setHash(hash) {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
|
||||
function hidePermissions() {
|
||||
let permissions = document.getElementById('permissions_settings');
|
||||
while (permissions.firstChild) {
|
||||
permissions.removeChild(permissions.firstChild);
|
||||
}
|
||||
permissions.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
function showPermissions() {
|
||||
let permissions = document.getElementById('permissions_settings');
|
||||
|
||||
let container = document.createElement('div');
|
||||
container.classList.add('permissions_contents');
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode('This app has the following permission:'));
|
||||
for (let key of Object.keys(gPermissions || {})) {
|
||||
let row = document.createElement('div');
|
||||
|
||||
let span = document.createElement('span');
|
||||
span.appendChild(document.createTextNode(key));
|
||||
row.appendChild(span);
|
||||
|
||||
span = document.createElement('span');
|
||||
span.appendChild(document.createTextNode(': '));
|
||||
row.appendChild(span);
|
||||
|
||||
span = document.createElement('span');
|
||||
span.appendChild(document.createTextNode(gPermissions[key] ? '✅ Allowed' : '❌ Denied'));
|
||||
row.appendChild(span);
|
||||
|
||||
span = document.createElement('span');
|
||||
span.appendChild(document.createTextNode(' '));
|
||||
row.appendChild(span);
|
||||
|
||||
let button = document.createElement('button');
|
||||
button.innerText = 'Reset';
|
||||
button.onclick = function() {
|
||||
send({action: "resetPermission", permission: key});
|
||||
};
|
||||
row.appendChild(button);
|
||||
div.appendChild(row);
|
||||
}
|
||||
container.appendChild(div);
|
||||
|
||||
div = document.createElement('div');
|
||||
let button = document.createElement('button');
|
||||
button.innerText = 'Close';
|
||||
button.onclick = function() {
|
||||
hidePermissions();
|
||||
}
|
||||
div.appendChild(button);
|
||||
container.appendChild(div);
|
||||
|
||||
permissions.appendChild(container);
|
||||
permissions.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
function _receive_websocket_message(message) {
|
||||
if (message && message.action == "session") {
|
||||
setStatusMessage("🟢 Executing...", kStatusColor);
|
||||
gCredentials = message.credentials;
|
||||
updateLogin();
|
||||
document.getElementsByTagName('tf-navigation')[0].credentials = message.credentials;
|
||||
} else if (message && message.action == 'permissions') {
|
||||
gPermissions = message.permissions;
|
||||
let permissions = document.getElementById('permissions_settings');
|
||||
if (permissions.firstChild) {
|
||||
hidePermissions();
|
||||
showPermissions();
|
||||
}
|
||||
document.getElementsByTagName('tf-navigation')[0].permissions = message.permissions ?? {};
|
||||
} else if (message && message.action == "ready") {
|
||||
setStatusMessage(null);
|
||||
if (window.location.hash) {
|
||||
send({event: "hashChange", hash: window.location.hash});
|
||||
}
|
||||
if (window.localStorage.getItem('stats') == '1') {
|
||||
/* Stats were opened before we connected. */
|
||||
send({action: 'enableStats', enabled: true});
|
||||
}
|
||||
send({action: 'enableStats', enabled: true});
|
||||
} else if (message && message.action == "ping") {
|
||||
send({action: "pong"});
|
||||
} else if (message && message.action == "stats") {
|
||||
@ -544,6 +775,9 @@ function _receive_websocket_message(message) {
|
||||
rpc_in: {group: 'rpc', name: 'in'},
|
||||
rpc_out: {group: 'rpc', name: 'out'},
|
||||
|
||||
cpu_percent: {group: 'cpu', name: 'main'},
|
||||
thread_percent: {group: 'cpu', name: 'work'},
|
||||
|
||||
arena_percent: {group: 'memory', name: 'm'},
|
||||
js_malloc_percent: {group: 'memory', name: 'js'},
|
||||
memory_percent: {group: 'memory', name: 'tot'},
|
||||
@ -552,8 +786,8 @@ function _receive_websocket_message(message) {
|
||||
tls_malloc_percent: {group: 'memory', name: 'tls'},
|
||||
uv_malloc_percent: {group: 'memory', name: 'uv'},
|
||||
|
||||
messages_stored: {group: 'stored', name: 'messages'},
|
||||
blobs_stored: {group: 'stored', name: 'blobs'},
|
||||
messages_stored: {group: 'store', name: 'messages'},
|
||||
blobs_stored: {group: 'store', name: 'blobs'},
|
||||
|
||||
socket_count: {group: 'socket', name: 'total'},
|
||||
socket_open_count: {group: 'socket', name: 'open'},
|
||||
@ -613,6 +847,16 @@ function _receive_websocket_message(message) {
|
||||
}
|
||||
}
|
||||
timeseries.append(now, message.stats[key]);
|
||||
|
||||
if (graph_key == 'cpu' || graph_key == 'rpc' || graph_key == 'store') {
|
||||
let line = document.getElementsByTagName('tf-navigation')[0].get_spark_line(graph_key, { max: 100 });
|
||||
line.dataset.emoji = {
|
||||
'cpu': '💻',
|
||||
'rpc': '🔁',
|
||||
'store': '💾',
|
||||
}[graph_key];
|
||||
line.append(key, message.stats[key]);
|
||||
}
|
||||
}
|
||||
} else if (message &&
|
||||
message.message === 'tfrpc' &&
|
||||
@ -638,27 +882,8 @@ function _receive_websocket_message(message) {
|
||||
}
|
||||
}
|
||||
|
||||
function keyEvent(event) {
|
||||
send({
|
||||
event: "key",
|
||||
type: event.type,
|
||||
which: event.which,
|
||||
keyCode: event.keyCode,
|
||||
charCode: event.charCode,
|
||||
character: String.fromCharCode(event.keyCode || event.which),
|
||||
altKey: event.altKey,
|
||||
});
|
||||
}
|
||||
|
||||
function setStatusMessage(message, color) {
|
||||
let node = document.getElementById("status");
|
||||
while (node.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
if (message) {
|
||||
node.appendChild(document.createTextNode(message));
|
||||
node.setAttribute("style", "display: inline-block; vertical-align: top; white-space: pre; color: " + (color || kErrorColor));
|
||||
}
|
||||
document.getElementsByTagName('tf-navigation')[0].status = {message: message, color: color};
|
||||
}
|
||||
|
||||
function send(value) {
|
||||
@ -671,23 +896,6 @@ function send(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateLogin() {
|
||||
let login = document.getElementById("login");
|
||||
while (login.firstChild) {
|
||||
login.removeChild(login.firstChild);
|
||||
}
|
||||
|
||||
let a = document.createElement("a");
|
||||
if (gCredentials && gCredentials.session) {
|
||||
a.appendChild(document.createTextNode("logout " + gCredentials.session.name));
|
||||
a.setAttribute("href", "/login/logout?return=" + encodeURIComponent(url() + hash()));
|
||||
} else {
|
||||
a.appendChild(document.createTextNode("login"));
|
||||
a.setAttribute("href", "/login?return=" + encodeURIComponent(url() + hash()));
|
||||
}
|
||||
login.appendChild(a);
|
||||
}
|
||||
|
||||
function dragHover(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@ -828,10 +1036,12 @@ function message(event) {
|
||||
function reconnect(path) {
|
||||
let oldSocket = gSocket;
|
||||
gSocket = null
|
||||
oldSocket.onopen = null;
|
||||
oldSocket.onclose = null;
|
||||
oldSocket.onmessage = null;
|
||||
oldSocket.close();
|
||||
if (oldSocket) {
|
||||
oldSocket.onopen = null;
|
||||
oldSocket.onclose = null;
|
||||
oldSocket.onmessage = null;
|
||||
oldSocket.close();
|
||||
}
|
||||
connectSocket(path);
|
||||
}
|
||||
|
||||
@ -895,27 +1105,13 @@ function openFile(name) {
|
||||
gEditor.focus();
|
||||
}
|
||||
|
||||
function onFileClicked(event) {
|
||||
openFile(event.target.textContent);
|
||||
}
|
||||
|
||||
function updateFiles() {
|
||||
let node = document.getElementById("files");
|
||||
while (node.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
|
||||
for (let file of Object.keys(gFiles).sort()) {
|
||||
let li = document.createElement("li");
|
||||
li.onclick = onFileClicked;
|
||||
li.appendChild(document.createTextNode(file));
|
||||
if (file == gCurrentFile) {
|
||||
li.classList.add("current");
|
||||
}
|
||||
if (!gFiles[file].doc.isClean(gFiles[file].generation)) {
|
||||
li.classList.add("dirty");
|
||||
}
|
||||
node.appendChild(li);
|
||||
let files = document.getElementsByTagName("tf-files-pane")[0];
|
||||
if (files) {
|
||||
files.files = Object.fromEntries(Object.keys(gFiles).map(file => [file, {
|
||||
clean: gFiles[file].doc.isClean(gFiles[file].generation),
|
||||
}]));
|
||||
files.current = gCurrentFile;
|
||||
}
|
||||
|
||||
gEditor.focus();
|
||||
@ -950,27 +1146,19 @@ window.addEventListener("load", function() {
|
||||
window.addEventListener("message", message, false);
|
||||
window.addEventListener("online", connectSocket);
|
||||
document.getElementById("name").value = window.location.pathname;
|
||||
document.getElementById('edit_link').addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
toggleEdit();
|
||||
});
|
||||
document.getElementById('show_permissions_link').addEventListener('click', () => showPermissions());
|
||||
document.getElementById('files_hide').addEventListener('click', () => hideFiles());
|
||||
document.getElementById('files_show').addEventListener('click', () => showFiles());
|
||||
document.getElementById('closeStats').addEventListener('click', () => closeStats());
|
||||
document.getElementById('closeEditor').addEventListener('click', () => closeEditor());
|
||||
document.getElementById('save').addEventListener('click', () => save());
|
||||
document.getElementById('icon').addEventListener('click', () => changeIcon());
|
||||
document.getElementById('delete').addEventListener('click', () => deleteApp());
|
||||
document.getElementById('trace_button').addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
trace();
|
||||
});
|
||||
document.getElementById('stats_button').addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
toggleStats();
|
||||
});
|
||||
document.getElementById('new_file_button').addEventListener('click', () => newFile());
|
||||
document.getElementById('remove_file_button').addEventListener('click', () => removeFile());
|
||||
for (let tag of document.getElementsByTagName('a')) {
|
||||
if (tag.accessKey) {
|
||||
tag.classList.add('tooltip_parent');
|
||||
@ -1003,11 +1191,6 @@ window.addEventListener("load", function() {
|
||||
} else {
|
||||
closeEditor();
|
||||
}
|
||||
if (window.localStorage.getItem('files') == '1') {
|
||||
showFiles();
|
||||
} else {
|
||||
hideFiles();
|
||||
}
|
||||
if (window.localStorage.getItem('stats') == '1') {
|
||||
stats();
|
||||
} else {
|
||||
|
375
core/core.js
375
core/core.js
@ -1,11 +1,45 @@
|
||||
import * as auth from './auth.js';
|
||||
import * as app from './app.js';
|
||||
import * as auth from './auth.js';
|
||||
import * as form from './form.js';
|
||||
import * as httpd from './httpd.js';
|
||||
|
||||
let gProcessIndex = 0;
|
||||
let gProcesses = {};
|
||||
let gStatsTimer = false;
|
||||
|
||||
const k_mime_types = {
|
||||
'css': 'text/css',
|
||||
'html': 'text/html',
|
||||
'js': 'text/javascript',
|
||||
'json': 'text/json',
|
||||
'map': 'application/json',
|
||||
'svg': 'image/svg+xml',
|
||||
};
|
||||
|
||||
const k_magic_bytes = [
|
||||
{bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'},
|
||||
{bytes: [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01], type: 'image/jpeg'},
|
||||
{bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'},
|
||||
{bytes: [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00], type: 'image/jpeg'},
|
||||
{bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'},
|
||||
{bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'},
|
||||
{bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'},
|
||||
{bytes: [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], type: 'image/webp'},
|
||||
{bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'},
|
||||
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'audio/mpeg'},
|
||||
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d], type: 'video/mp4'},
|
||||
{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'video/mp4'},
|
||||
];
|
||||
|
||||
let k_static_files = [
|
||||
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
||||
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
|
||||
{uri: '/favicon.png', type: 'image/png'},
|
||||
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
|
||||
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
|
||||
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
|
||||
];
|
||||
|
||||
const k_global_settings = {
|
||||
index: {
|
||||
type: 'string',
|
||||
@ -38,8 +72,6 @@ let gGlobalSettings = {
|
||||
index: "/~core/apps/",
|
||||
};
|
||||
|
||||
let kGlobalSettingsFile = "data/global/settings.json";
|
||||
|
||||
let kPingInterval = 60 * 1000;
|
||||
|
||||
function printError(out, error) {
|
||||
@ -47,8 +79,8 @@ function printError(out, error) {
|
||||
out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
|
||||
out.print(error.stackTrace);
|
||||
} else {
|
||||
for (let i in error) {
|
||||
out.print(i);
|
||||
for (let [k, v] of Object.entries(error)) {
|
||||
out.print(k, v);
|
||||
}
|
||||
out.print(error.toString());
|
||||
}
|
||||
@ -145,12 +177,12 @@ async function getSessionProcessBlob(blobId, session, options) {
|
||||
return getProcessBlob(blobId, 'session_' + session, actualOptions);
|
||||
}
|
||||
|
||||
let gManifestCache = {};
|
||||
|
||||
async function getProcessBlob(blobId, key, options) {
|
||||
let process = gProcesses[key];
|
||||
if (!process
|
||||
&& !(options && "create" in options && !options.create)) {
|
||||
let resolveReady;
|
||||
let rejectReady;
|
||||
try {
|
||||
print("Creating task for " + blobId + " " + key);
|
||||
process = {};
|
||||
@ -160,13 +192,13 @@ async function getProcessBlob(blobId, key, options) {
|
||||
process.credentials = options.credentials || {};
|
||||
process.task = new Task();
|
||||
process.eventHandlers = {};
|
||||
process.app = new app.App();
|
||||
if (!options?.script || options?.script === 'app.js') {
|
||||
process.app = new app.App();
|
||||
}
|
||||
process.lastActive = Date.now();
|
||||
process.lastPing = null;
|
||||
process.timeout = options.timeout;
|
||||
process.stats = false;
|
||||
let resolveReady;
|
||||
let rejectReady;
|
||||
process.ready = new Promise(function(resolve, reject) {
|
||||
resolveReady = resolve;
|
||||
rejectReady = reject;
|
||||
@ -246,7 +278,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
}
|
||||
|
||||
} else {
|
||||
} else if (process.app) {
|
||||
return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
|
||||
if (value == 'allow') {
|
||||
storePermission(user, options.packageOwner, options.packageName, permission, true);
|
||||
@ -263,6 +295,8 @@ async function getProcessBlob(blobId, key, options) {
|
||||
}
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
});
|
||||
} else {
|
||||
throw Error(`Permission denied: ${permission}.`);
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -313,14 +347,23 @@ async function getProcessBlob(blobId, key, options) {
|
||||
imports.app[api[0]] = process.app.makeFunction(api);
|
||||
}
|
||||
}
|
||||
for (let [name, f] of Object.entries(options?.imports || {})) {
|
||||
imports[name] = f;
|
||||
}
|
||||
process.task.onPrint = function(args) {
|
||||
imports.app.print(...args);
|
||||
if (imports.app) {
|
||||
imports.app.print(...args);
|
||||
}
|
||||
};
|
||||
process.task.onError = function(error) {
|
||||
try {
|
||||
process.app.makeFunction(['error'])(error);
|
||||
} catch(e) {
|
||||
print(e);
|
||||
if (process.app) {
|
||||
process.app.makeFunction(['error'])(error);
|
||||
} else {
|
||||
printError({print: print}, error);
|
||||
}
|
||||
} catch (e) {
|
||||
printError({print: print}, error);
|
||||
}
|
||||
};
|
||||
imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
|
||||
@ -347,6 +390,20 @@ async function getProcessBlob(blobId, key, options) {
|
||||
});
|
||||
}
|
||||
};
|
||||
imports.ssb.privateMessageEncrypt = function(id, recipients, message) {
|
||||
if (process.credentials &&
|
||||
process.credentials.session &&
|
||||
process.credentials.session.name) {
|
||||
return ssb.privateMessageEncrypt(process.credentials.session.name, id, recipients, message);
|
||||
}
|
||||
};
|
||||
imports.ssb.privateMessageDecrypt = function(id, message) {
|
||||
if (process.credentials &&
|
||||
process.credentials.session &&
|
||||
process.credentials.session.name) {
|
||||
return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
|
||||
}
|
||||
};
|
||||
|
||||
if (process.credentials &&
|
||||
process.credentials.session &&
|
||||
@ -385,7 +442,7 @@ async function getProcessBlob(blobId, key, options) {
|
||||
try {
|
||||
let appObject = JSON.parse(appSource);
|
||||
if (appObject.type == "tildefriends-app") {
|
||||
appSourceName = 'app.js';
|
||||
appSourceName = options?.script ?? 'app.js';
|
||||
let id = appObject.files[appSourceName];
|
||||
let blob = await getBlobOrContent(id);
|
||||
appSource = utf8Decode(blob);
|
||||
@ -398,19 +455,23 @@ async function getProcessBlob(blobId, key, options) {
|
||||
printError({print: print}, e);
|
||||
}
|
||||
broadcastEvent('onSessionBegin', [getUser(process, process)]);
|
||||
resolveReady(process);
|
||||
if (process.app) {
|
||||
process.app.send({action: "ready"});
|
||||
process.sendPermissions();
|
||||
}
|
||||
await process.task.execute({name: appSourceName, source: appSource});
|
||||
resolveReady(process);
|
||||
} catch (error) {
|
||||
if (process?.task?.onError) {
|
||||
process.task.onError(error);
|
||||
if (process.app) {
|
||||
if (process?.task?.onError) {
|
||||
process.task.onError(error);
|
||||
} else {
|
||||
printError({print: print}, error);
|
||||
}
|
||||
} else {
|
||||
printError({print: print}, error);
|
||||
}
|
||||
rejectReady();
|
||||
rejectReady(error);
|
||||
}
|
||||
}
|
||||
return process;
|
||||
@ -425,20 +486,11 @@ function setGlobalSettings(settings) {
|
||||
}
|
||||
}
|
||||
|
||||
let kStaticFiles = [
|
||||
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
||||
{uri: '/style.css', type: 'text/css; charset=UTF-8'},
|
||||
{uri: '/favicon.png', type: 'image/png'},
|
||||
{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
|
||||
{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
|
||||
{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
|
||||
];
|
||||
|
||||
function startsWithBytes(data, bytes) {
|
||||
if (data.byteLength >= bytes.length) {
|
||||
let dataBytes = new Uint8Array(data.slice(0, bytes.length));
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (dataBytes[i] != bytes[i] && bytes[i] !== null) {
|
||||
if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -447,16 +499,16 @@ function startsWithBytes(data, bytes) {
|
||||
}
|
||||
|
||||
async function staticFileHandler(request, response, blobId, uri) {
|
||||
for (let i in kStaticFiles) {
|
||||
if (uri === kStaticFiles[i].uri) {
|
||||
let path = kStaticFiles[i].path || uri.substring(1);
|
||||
let type = kStaticFiles[i].type || guessType(path);
|
||||
for (let i in k_static_files) {
|
||||
if (uri === k_static_files[i].uri) {
|
||||
let path = k_static_files[i].path || uri.substring(1);
|
||||
let type = k_static_files[i].type || guessTypeFromName(path);
|
||||
|
||||
let stat = await File.stat('core/' + path);
|
||||
let id = `${stat.mtime}_${stat.size}`;
|
||||
|
||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||
response.writeHead(304, {});
|
||||
response.writeHead(304, {'Content-Length': '0'});
|
||||
response.end();
|
||||
} else {
|
||||
let data = await File.readFile('core/' + path);
|
||||
@ -466,7 +518,7 @@ async function staticFileHandler(request, response, blobId, uri) {
|
||||
'Content-Length': data.byteLength,
|
||||
'etag': '"' + id + '"',
|
||||
},
|
||||
kStaticFiles[i].headers || {}));
|
||||
k_static_files[i].headers || {}));
|
||||
response.end(data);
|
||||
}
|
||||
return;
|
||||
@ -477,14 +529,6 @@ async function staticFileHandler(request, response, blobId, uri) {
|
||||
response.end("File not found");
|
||||
}
|
||||
|
||||
const k_mime_types = {
|
||||
'json': 'text/json',
|
||||
'js': 'text/javascript',
|
||||
'html': 'text/html',
|
||||
'css': 'text/css',
|
||||
'map': 'application/json',
|
||||
};
|
||||
|
||||
async function staticDirectoryHandler(request, response, directory, uri) {
|
||||
let filename = uri || 'index.html';
|
||||
if (filename.indexOf('..') != -1) {
|
||||
@ -498,7 +542,7 @@ async function staticDirectoryHandler(request, response, directory, uri) {
|
||||
let id = `${stat.mtime}_${stat.size}`;
|
||||
|
||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||
response.writeHead(304, {});
|
||||
response.writeHead(304, {'Content-Length': '0'});
|
||||
response.end();
|
||||
} else {
|
||||
let data = await File.readFile(directory + filename);
|
||||
@ -526,35 +570,23 @@ async function wellKnownHandler(request, response, path) {
|
||||
}
|
||||
}
|
||||
|
||||
function guessTypeFromName(path) {
|
||||
let extension = path.split('.').pop();
|
||||
return k_mime_types[extension];
|
||||
}
|
||||
|
||||
function guessTypeFromMagicBytes(data) {
|
||||
for (let magic of k_magic_bytes) {
|
||||
if (startsWithBytes(data, magic.bytes)) {
|
||||
return magic.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendData(response, data, type, headers) {
|
||||
if (data) {
|
||||
if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||
startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "image/jpeg", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "image/png", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||
startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "image/gif", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else if (startsWithBytes(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "image/webp", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "audio/mpeg", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else if (startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
||||
startsWithBytes(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||
response.writeHead(200, Object.assign({"Content-Type": "video/mp4", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else {
|
||||
response.writeHead(200, Object.assign({"Content-Type": type || "application/binary", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
}
|
||||
response.writeHead(200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {}));
|
||||
response.end(data);
|
||||
} else {
|
||||
response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
|
||||
response.end("File not found");
|
||||
@ -571,35 +603,53 @@ async function getBlobOrContent(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function guessType(path) {
|
||||
const k_extension_to_type = {
|
||||
'css': 'text/css',
|
||||
'html': 'text/html',
|
||||
'js': 'text/javascript',
|
||||
'svg': 'image/svg+xml',
|
||||
};
|
||||
let extension = path.split('.').pop();
|
||||
return k_extension_to_type[extension];
|
||||
let g_handler_index = 0;
|
||||
async function useAppHandler(response, handler_blob_id, path) {
|
||||
let do_resolve;
|
||||
let promise = new Promise(async function(resolve, reject) {
|
||||
do_resolve = resolve;
|
||||
});
|
||||
let process;
|
||||
let result;
|
||||
try {
|
||||
process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, {
|
||||
script: 'handler.js',
|
||||
imports: {
|
||||
request: {
|
||||
path: path,
|
||||
},
|
||||
respond: do_resolve,
|
||||
},
|
||||
});
|
||||
await process.ready;
|
||||
|
||||
result = await promise;
|
||||
} finally {
|
||||
if (process?.task) {
|
||||
await process.task.kill();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function blobHandler(request, response, blobId, uri) {
|
||||
for (let i in kStaticFiles) {
|
||||
if (uri === kStaticFiles[i].uri && kStaticFiles[i].path) {
|
||||
let stat = await File.stat('core/' + kStaticFiles[i].path);
|
||||
for (let i in k_static_files) {
|
||||
if (uri === k_static_files[i].uri && k_static_files[i].path) {
|
||||
let stat = await File.stat('core/' + k_static_files[i].path);
|
||||
let id = `${stat.mtime}_${stat.size}`;
|
||||
|
||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||
response.writeHead(304, {});
|
||||
response.writeHead(304, {'Content-Length': '0'});
|
||||
response.end();
|
||||
} else {
|
||||
let data = await File.readFile('core/' + kStaticFiles[i].path);
|
||||
let data = await File.readFile('core/' + k_static_files[i].path);
|
||||
response.writeHead(200, Object.assign(
|
||||
{
|
||||
'Content-Type': kStaticFiles[i].type,
|
||||
'Content-Type': k_static_files[i].type,
|
||||
'Content-Length': data.byteLength,
|
||||
'etag': '"' + id + '"',
|
||||
},
|
||||
kStaticFiles[i].headers || {}));
|
||||
k_static_files[i].headers || {}));
|
||||
response.end(data);
|
||||
}
|
||||
return;
|
||||
@ -616,11 +666,19 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
if (uri == "/view") {
|
||||
let data;
|
||||
let match;
|
||||
let query = form.decodeForm(request.query);
|
||||
let headers = {
|
||||
'Content-Security-Policy': 'sandbox',
|
||||
};
|
||||
if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
|
||||
headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
|
||||
}
|
||||
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
||||
let id = await new Database(match[1]).get('path:' + match[2]);
|
||||
if (id) {
|
||||
if (request.headers['if-none-match'] === '"' + id + '"') {
|
||||
response.writeHead(304, {});
|
||||
headers['Content-Length'] = '0';
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
data = await getBlobOrContent(id);
|
||||
@ -628,23 +686,25 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
let appObject = JSON.parse(data);
|
||||
data = appObject.files[match[3]];
|
||||
}
|
||||
sendData(response, data, undefined, {etag: '"' + id + '"'});
|
||||
sendData(response, data, undefined, Object.assign({etag: '"' + id + '"'}, headers));
|
||||
}
|
||||
} else {
|
||||
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
||||
response.writeHead(304, {});
|
||||
headers['Content-Length'] = '0';
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
|
||||
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (request.headers['if-none-match'] === '"' + blobId + '"') {
|
||||
response.writeHead(304, {});
|
||||
headers['Content-Length'] = '0';
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
data = await getBlobOrContent(blobId);
|
||||
sendData(response, data, undefined, {etag: '"' + blobId + '"'});
|
||||
sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
|
||||
}
|
||||
}
|
||||
} else if (uri == "/save") {
|
||||
@ -705,7 +765,7 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
} catch {
|
||||
}
|
||||
if (apps.delete(appName)) {
|
||||
database.set('apps', JSON.stringify([...apps]));
|
||||
database.set('apps', JSON.stringify([...apps].sort()));
|
||||
}
|
||||
database.remove('path:' + appName);
|
||||
} else {
|
||||
@ -719,43 +779,54 @@ async function blobHandler(request, response, blobId, uri) {
|
||||
response.end('OK');
|
||||
} else {
|
||||
let data;
|
||||
let type;
|
||||
let headers;
|
||||
let match;
|
||||
let id;
|
||||
let app_id = blobId;
|
||||
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
|
||||
let db = new Database(match[1]);
|
||||
let id = await db.get('path:' + match[2]);
|
||||
if (id) {
|
||||
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
|
||||
headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
data = utf8Decode(await getBlobOrContent(id));
|
||||
let appObject = JSON.parse(data);
|
||||
data = appObject.files[uri.substring(1)];
|
||||
data = await getBlobOrContent(data);
|
||||
type = guessType(uri);
|
||||
headers = {
|
||||
'ETag': '"' + id + '"',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
sendData(response, data, type, headers);
|
||||
}
|
||||
app_id = await db.get('path:' + match[2]);
|
||||
}
|
||||
|
||||
let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
|
||||
id = app_object.files[uri.substring(1)];
|
||||
if (!id && app_object.files['handler.js']) {
|
||||
let answer;
|
||||
try {
|
||||
answer = await useAppHandler(response, app_id, uri.substring(1));
|
||||
} catch (error) {
|
||||
data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`);
|
||||
response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length});
|
||||
response.end(data);
|
||||
return;
|
||||
}
|
||||
if (answer && typeof answer.data == 'string') {
|
||||
answer.data = utf8Encode(answer.data);
|
||||
}
|
||||
sendData(response, answer?.data, answer?.content_type, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': 'sandbox',
|
||||
});
|
||||
} else if (id) {
|
||||
if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
|
||||
let headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': 'sandbox',
|
||||
'Content-Length': '0',
|
||||
};
|
||||
response.writeHead(304, headers);
|
||||
response.end();
|
||||
} else {
|
||||
let headers = {
|
||||
'ETag': '"' + id + '"',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Security-Policy': 'sandbox',
|
||||
};
|
||||
data = await getBlobOrContent(id);
|
||||
let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
|
||||
sendData(response, data, type, headers);
|
||||
}
|
||||
} else {
|
||||
data = utf8Decode(await getBlobOrContent(blobId));
|
||||
let appObject = JSON.parse(data);
|
||||
data = appObject.files[uri.substring(1)];
|
||||
data = await getBlobOrContent(data);
|
||||
headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
sendData(response, data, type, headers);
|
||||
sendData(response, data, undefined, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -769,8 +840,7 @@ ssb.addEventListener('connections', function() {
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
let data;
|
||||
|
||||
let data = {};
|
||||
try {
|
||||
let settings = new Database('core').get('settings');
|
||||
if (settings) {
|
||||
@ -779,19 +849,12 @@ async function loadSettings() {
|
||||
} catch (error) {
|
||||
print("Settings not found in database:", error);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
try {
|
||||
data = JSON.parse(utf8Decode(await File.readFile(kGlobalSettingsFile)));
|
||||
new Database('core').set('settings', JSON.stringify(data));
|
||||
} catch (error) {
|
||||
print("Unable to load settings from " + kGlobalSettingsFile + ":", error);
|
||||
for (let [key, value] of Object.entries(k_global_settings)) {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = value.default_value;
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
gGlobalSettings = data;
|
||||
}
|
||||
gGlobalSettings = data;
|
||||
}
|
||||
|
||||
function sendStats() {
|
||||
@ -815,6 +878,16 @@ function enableStats(process, enabled) {
|
||||
}
|
||||
}
|
||||
|
||||
function stringResponse(response, data) {
|
||||
let bytes = utf8Encode(data);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Length": bytes.byteLength.toString(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
return response.end(bytes);
|
||||
}
|
||||
|
||||
loadSettings().then(function() {
|
||||
httpd.all("/login", auth.handler);
|
||||
httpd.all("", function(request, response) {
|
||||
@ -826,8 +899,8 @@ loadSettings().then(function() {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
|
||||
return staticFileHandler(request, response, null, match[1]);
|
||||
} else if (match = /^\/static\/lit\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||
return staticDirectoryHandler(request, response, 'deps/lit/', match[1]);
|
||||
} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||
return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
|
||||
} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||
@ -836,22 +909,22 @@ loadSettings().then(function() {
|
||||
return staticDirectoryHandler(request, response, 'deps/split/', match[1]);
|
||||
} else if (match = /^\/smoothie\/([\.\w-/]*)$/.exec(request.uri)) {
|
||||
return staticDirectoryHandler(request, response, 'deps/smoothie/', match[1]);
|
||||
} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
|
||||
return staticFileHandler(request, response, null, match[1]);
|
||||
} else if (request.uri == "/robots.txt") {
|
||||
return staticFileHandler(request, response, null, request.uri);
|
||||
} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
|
||||
return blobHandler(request, response, match[1], match[2]);
|
||||
} else if (match = /^\/trace$/.exec(request.uri)) {
|
||||
let data = trace();
|
||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
|
||||
return response.end(data);
|
||||
return stringResponse(response, trace());
|
||||
} else if (match = /^\/disconnections$/.exec(request.uri)) {
|
||||
let data = utf8Encode(JSON.stringify(disconnectionsDebug(), null, 2));
|
||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.byteLength.toString()});
|
||||
return response.end(data);
|
||||
return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
|
||||
} else if (match = /^\/debug$/.exec(request.uri)) {
|
||||
let data = JSON.stringify(getDebug(), null, 2);
|
||||
response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
|
||||
return response.end(data);
|
||||
} else if (request.uri == "/robots.txt") {
|
||||
return blobHandler(request, response, null, request.uri);
|
||||
return stringResponse(response, JSON.stringify(getDebug(), null, 2));
|
||||
} else if (match = /^\/hitches$/.exec(request.uri)) {
|
||||
return stringResponse(response, JSON.stringify(getHitches(), null, 2));
|
||||
} else if (match = /^\/mem$/.exec(request.uri)) {
|
||||
return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
|
||||
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
|
||||
return wellKnownHandler(request, response, match[1]);
|
||||
} else {
|
||||
|
20
core/form.js
20
core/form.js
@ -1,7 +1,7 @@
|
||||
function decode(encoded) {
|
||||
var result = "";
|
||||
for (var i = 0; i < encoded.length; i++) {
|
||||
var c = encoded[i];
|
||||
let result = "";
|
||||
for (let i = 0; i < encoded.length; i++) {
|
||||
let c = encoded[i];
|
||||
if (c == "+") {
|
||||
result += " ";
|
||||
} else if (c == "%") {
|
||||
@ -15,15 +15,15 @@ function decode(encoded) {
|
||||
}
|
||||
|
||||
function decodeForm(encoded, initial) {
|
||||
var result = initial || {};
|
||||
let result = initial || {};
|
||||
if (encoded) {
|
||||
encoded = encoded.trim();
|
||||
var items = encoded.split('&');
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
var equals = item.indexOf('=');
|
||||
var key = decode(item.slice(0, equals));
|
||||
var value = decode(item.slice(equals + 1));
|
||||
let items = encoded.split('&');
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
let equals = item.indexOf('=');
|
||||
let key = decode(item.slice(0, equals));
|
||||
let value = decode(item.slice(equals + 1));
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
@ -456,8 +456,6 @@ function handleConnection(client) {
|
||||
}
|
||||
}
|
||||
|
||||
client.noDelay = true;
|
||||
|
||||
client.onError(function(error) {
|
||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||
});
|
||||
@ -495,7 +493,7 @@ function handleConnection(client) {
|
||||
parsing_header = false;
|
||||
inputBuffer = inputBuffer.slice(result.bytes_parsed);
|
||||
|
||||
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect) {
|
||||
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) {
|
||||
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||
let response = new Response(requestObject, client);
|
||||
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
|
||||
@ -557,11 +555,25 @@ let kBacklog = 8;
|
||||
let kHost = "0.0.0.0"
|
||||
|
||||
let socket = new Socket();
|
||||
socket.bind(kHost, tildefriends.http_port).then(function() {
|
||||
let listenResult = socket.listen(kBacklog, function() {
|
||||
socket.accept().then(handleConnection).catch(function(error) {
|
||||
logError("[" + new Date() + "] accept error " + error);
|
||||
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
||||
print("bound to", port);
|
||||
print("checking", tildefriends.args.out_http_port_file);
|
||||
if (tildefriends.args.out_http_port_file) {
|
||||
print("going to write the file");
|
||||
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
||||
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
||||
}).catch(function() {
|
||||
print("failed to write port file");
|
||||
});
|
||||
}
|
||||
let listenResult = socket.listen(kBacklog, async function() {
|
||||
try {
|
||||
let client = await socket.accept();
|
||||
client.noDelay = true;
|
||||
handleConnection(client);
|
||||
} catch (error) {
|
||||
logError("[" + new Date() + "] accept error " + error);
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
logError("[" + new Date() + "] bind error " + error);
|
||||
@ -574,6 +586,7 @@ if (tildefriends.https_port) {
|
||||
return secureSocket.listen(kBacklog, async function() {
|
||||
try {
|
||||
let client = await secureSocket.accept();
|
||||
client.noDelay = true;
|
||||
client.tls = true;
|
||||
const kCertificatePath = "data/httpd/certificate.pem";
|
||||
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
||||
|
@ -7,18 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body style="display: flex; flex-flow: column">
|
||||
<div class="navigation">
|
||||
<span>😎</span>
|
||||
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff">Tilde Friends</a>
|
||||
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||
<a accesskey="e" data-tip="Toggle the app editor." href="#" id="edit_link">edit</a>
|
||||
<a accesskey="p" data-tip="View and change permissions." href="#" id="show_permissions_link">🎛️</a>
|
||||
<span id="status"></span>
|
||||
<span id="requests"></span>
|
||||
<span id="permissions_settings"></span>
|
||||
<span id="permissions"></span>
|
||||
<span id="login"></span>
|
||||
</div>
|
||||
<tf-navigation></tf-navigation>
|
||||
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
|
||||
<div id="statsPane" class="vbox" style="display: none; flex 1 1">
|
||||
<div class="hbox">
|
||||
@ -30,25 +19,14 @@
|
||||
<div class="navigation hbox">
|
||||
<input type="button" id="closeEditor" name="closeEditor" value="Close">
|
||||
<input type="button" id="save" name="save" value="Save">
|
||||
<input type="button" id="icon" name="icon" value="📦">
|
||||
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
||||
<input type="button" id="delete" name="delete" value="Delete">
|
||||
<input type="button" id="trace_button" value="Trace">
|
||||
<input type="button" id="stats_button" value="Stats">
|
||||
</div>
|
||||
<div class="hbox" style="height: 100%">
|
||||
<div id="filesPane">
|
||||
<div class="hbox">
|
||||
<span id="files_header">Files</span>
|
||||
<span id="files_hide">«</span>
|
||||
<span id="files_show">»</span>
|
||||
</div>
|
||||
<div id="files_content">
|
||||
<ul id="files"></ul>
|
||||
<br>
|
||||
<div><button id="new_file_button">New File</button></div>
|
||||
<div><button id="remove_file_button">Remove File</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<tf-files-pane></tf-files-pane>
|
||||
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
|
||||
<div style="flex: 1 1 50%; position: relative">
|
||||
<textarea id="editor" class="main"></textarea>
|
||||
@ -60,6 +38,7 @@
|
||||
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<script>window.litDisableBundleWarning = true;</script>
|
||||
<script src="/split/split.min.js"></script>
|
||||
<script src="/smoothie/smoothie.js"></script>
|
||||
<script src="/static/client.js" type="module"></script>
|
||||
|
@ -15,11 +15,6 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
height: auto;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #268bd2;
|
||||
}
|
||||
@ -162,70 +157,6 @@ a:active {
|
||||
.cyan { color: #2aa198; }
|
||||
.green { color: #859900; }
|
||||
|
||||
#files_header {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#files_hide {
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
right: 0;
|
||||
flex: 0;
|
||||
padding: 0.25em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#files_show {
|
||||
display: none;
|
||||
padding: 0.25em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#filesPane {
|
||||
flex: 1 1 10%;
|
||||
}
|
||||
|
||||
#filesPane.collapsed {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.collapsed #files_header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed #files_content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed #files_hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsed #files_show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#files {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#files > li {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
#files > li.current {
|
||||
font-weight: bold;
|
||||
background-color: #2aa198;
|
||||
}
|
||||
|
||||
#files > li.dirty::after {
|
||||
content: '*';
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
@ -254,8 +185,7 @@ kbd {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#permissions, #permissions_settings {
|
||||
visibility: hidden;
|
||||
.permissions {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
|
@ -39,9 +39,9 @@ function send(response) {
|
||||
|
||||
function call_rpc(message) {
|
||||
if (message && message.message === 'tfrpc') {
|
||||
let method = g_api[message.method];
|
||||
let id = message.id;
|
||||
if (message.method) {
|
||||
let method = g_api[message.method];
|
||||
if (method) {
|
||||
try {
|
||||
Promise.resolve(method(...message.params)).then(function(result) {
|
||||
@ -53,7 +53,7 @@ function call_rpc(message) {
|
||||
send({message: 'tfrpc', id: id, error: error});
|
||||
}
|
||||
} else {
|
||||
throw new Error(message.method + ' not found.');
|
||||
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`});
|
||||
}
|
||||
} else if (message.error !== undefined) {
|
||||
if (g_calls[id]) {
|
||||
|
106
deps/base64c/.gitignore
vendored
106
deps/base64c/.gitignore
vendored
@ -1,106 +0,0 @@
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
# http://www.gnu.org/software/automake
|
||||
Makefile
|
||||
Makefile.in
|
||||
/ar-lib
|
||||
/mdate-sh
|
||||
/py-compile
|
||||
/test-driver
|
||||
/ylwrap
|
||||
|
||||
# http://www.gnu.org/software/autoheader
|
||||
config.h
|
||||
# http://www.gnu.org/software/autoconf
|
||||
|
||||
autom4te.cache
|
||||
/autoscan.log
|
||||
/autoscan-*.log
|
||||
/aclocal.m4
|
||||
/compile
|
||||
/config.guess
|
||||
/config.h.in
|
||||
/config.log
|
||||
/config.status
|
||||
/config.sub
|
||||
/configure
|
||||
/configure.scan
|
||||
/depcomp
|
||||
/install-sh
|
||||
/missing
|
||||
/stamp-h1
|
||||
|
||||
# https://www.gnu.org/software/libtool/
|
||||
|
||||
/ltmain.sh
|
||||
|
||||
# http://www.gnu.org/software/texinfo
|
||||
|
||||
/texinfo.tex
|
||||
|
||||
# http://www.gnu.org/software/m4/
|
||||
|
||||
m4/libtool.m4
|
||||
m4/ltoptions.m4
|
||||
m4/ltsugar.m4
|
||||
m4/ltversion.m4
|
||||
m4/lt~obsolete.m4
|
||||
|
||||
# vim
|
||||
*.swp
|
||||
|
||||
# project specific
|
||||
test/gen
|
||||
test/test[0-9]*
|
||||
test/.deps
|
29
deps/base64c/LICENSE
vendored
29
deps/base64c/LICENSE
vendored
@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2018, Sean Hanna
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
2
deps/base64c/Makefile.am
vendored
2
deps/base64c/Makefile.am
vendored
@ -1,2 +0,0 @@
|
||||
AUTOMAKE_OPTIONS = foreign
|
||||
SUBDIRS = src test
|
60
deps/base64c/README.md
vendored
60
deps/base64c/README.md
vendored
@ -1,60 +0,0 @@
|
||||
# base64c
|
||||
This is primarily just a fork of a base64 decoder from the FreeBSD codebase. It has received a few modifications:
|
||||
* removed all allocations, you are expected to pass in a buffer that has sufficient space and you will get an error (-1) if you run out of space
|
||||
* replaced a dynamically generated lookup table with a hardcoded lookup table
|
||||
* wrote my own unit tests, i'm sure there are tests for freebsd somewhere but i didn't find them
|
||||
|
||||
# Embedding
|
||||
This code is primarily intended to be dropped into an existing code base ( or perhaps using submodules). To do that:
|
||||
|
||||
* grab include/base64c.h
|
||||
* grab src/base64c.h
|
||||
|
||||
# Usage
|
||||
|
||||
Call base64c_encoding_length() to calculate how big a buffer you need to encode a string. It's somewhere around 4 times the size of the input string. This length includes a null terminator.
|
||||
|
||||
```c
|
||||
char input_string[256];
|
||||
|
||||
size_t new_len = base64c_encoding_length( strlen(input_string));
|
||||
|
||||
unsigned char *buffer = (unsigned char*)malloc(new_len);
|
||||
```
|
||||
|
||||
Call base64c_encode() to actually encode your input string as base64. It will write to the buffer and return how many characters were written. If there was an error it will return -1.
|
||||
|
||||
```c
|
||||
size_t output_length = base64c_encode(input_string, strlen(input_string), buffer, new_len);
|
||||
|
||||
if (output_length == -1) {
|
||||
int x = 1/0; // ERROR!
|
||||
}
|
||||
```
|
||||
|
||||
Call base64c_decoding_length() to calculate how big a buffer you need to decode. It comes out to about half the size. This number isn't always exact, but it is close to within a byte or two.
|
||||
|
||||
```c
|
||||
size_t decode_len = base64c_decoding_length( strlen(buffer) );
|
||||
|
||||
unsigned char *decoded = (unsigned char*)malloc( decode_len );
|
||||
```
|
||||
|
||||
Call base64c_decode() to decode an encoded base64 string. It will write to the buffer and return how many characters were written. IF there was an error it will return -1. If the string contains invalid number of characters, or has any characters that are not part of the base64 character set an error will be returned.
|
||||
|
||||
# Building
|
||||
|
||||
You need to bootstrap all the autoconf tools by running ./autogen.sh
|
||||
|
||||
You need to have autoconf installed to do this.
|
||||
|
||||
Once bootstrapped run ./configure
|
||||
|
||||
# Tests
|
||||
|
||||
There are tests in the test/ subfolder. They will be built automatically. There is no special test runner. You can run each of the test cases manually to check whether the code is working properly.
|
||||
|
||||
# References
|
||||
|
||||
(http://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c)
|
||||
(https://github.com/freebsd/freebsd/blob/master/contrib/wpa/src/utils/base64.c)
|
3
deps/base64c/autogen.sh
vendored
3
deps/base64c/autogen.sh
vendored
@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
aclocal && automake --gnu --add-missing && autoconf
|
22
deps/base64c/configure.ac
vendored
22
deps/base64c/configure.ac
vendored
@ -1,22 +0,0 @@
|
||||
# -*- Autoconf -*-
|
||||
# Process this file with autoconf to produce a configure script.
|
||||
BASE64C_VERSION=0.5
|
||||
AC_PREREQ([2.69])
|
||||
AC_INIT(base64c, 0.5, hannasm@gmail.com)
|
||||
AM_INIT_AUTOMAKE(base64c, 0.5)
|
||||
AC_CONFIG_SRCDIR([include/base64c.h])
|
||||
AC_CONFIG_HEADERS([config.h])
|
||||
|
||||
# Checks for programs.
|
||||
AC_PROG_CC
|
||||
|
||||
# Checks for libraries.
|
||||
|
||||
# Checks for header files.
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_TYPE_SIZE_T
|
||||
|
||||
# Checks for library functions.
|
||||
|
||||
AC_OUTPUT(Makefile src/Makefile test/Makefile)
|
42
deps/base64c/include/base64c.h
vendored
42
deps/base64c/include/base64c.h
vendored
@ -1,42 +0,0 @@
|
||||
#ifndef base64cC_H
|
||||
#define base64cC_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* base64c_encoding_length - calculate length to allocate for encode
|
||||
* @len: Length of input string
|
||||
* Returns: number of bytes required to base64c encode, this includes room for '\0' terminator
|
||||
*/
|
||||
size_t base64c_encoding_length(size_t len);
|
||||
|
||||
/**
|
||||
* base64c_decoding_length - calculate length to allocate for decode
|
||||
* @len: Length of (base64 encoded) input string
|
||||
* Returns: maximum number of bytes required to decode
|
||||
*/
|
||||
size_t base64c_decoding_length(size_t inlen);
|
||||
|
||||
/**
|
||||
* base64c_encode - base64c encode
|
||||
* @src: Data to be encoded
|
||||
* @len: Length of the data to be encoded
|
||||
* @out: Mutable output buffer destination, all encoded bytes will be written to the destination
|
||||
* @out_len: length of output buffer
|
||||
* Returns: number of bytes written, or -1 if there was an error
|
||||
*/
|
||||
size_t base64c_encode(const unsigned char *src, size_t len, unsigned char* out, const size_t out_len);
|
||||
|
||||
/**
|
||||
* base64c_decode - base64c decode
|
||||
* @src: Data to be decoded
|
||||
* @len: Length of the data to be decoded
|
||||
* @out_len: Pointer to output length variable
|
||||
* Returns: Allocated buffer of out_len bytes of decoded data,
|
||||
* or %NULL on failure
|
||||
*
|
||||
* Caller is responsible for freeing the returned buffer.
|
||||
*/
|
||||
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len);
|
||||
#endif
|
3
deps/base64c/src/Makefile.am
vendored
3
deps/base64c/src/Makefile.am
vendored
@ -1,3 +0,0 @@
|
||||
CFLAGS = --pednatic -Wall -stdc99 -O2
|
||||
LDFLAGS =
|
||||
|
139
deps/base64c/src/base64c.c
vendored
139
deps/base64c/src/base64c.c
vendored
@ -1,139 +0,0 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/*
|
||||
* Base64 encoding/decoding (RFC1341)
|
||||
* Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
|
||||
*
|
||||
* This software may be distributed under the terms of the BSD license.
|
||||
* See README for more details.
|
||||
*/
|
||||
static const unsigned char base64c_table[65] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
static const unsigned char base64c_dtable[256] = {
|
||||
/*000*/0x80,/*001*/0x80,/*002*/0x80,/*003*/0x80,/*004*/0x80,/*005*/0x80,/*006*/0x80,/*007*/0x80,/*008*/0x80,/*009*/0x80,/*010*/0x80,/*011*/0x80,/*012*/0x80,/*013*/0x80,/*014*/0x80,/*015*/0x80,/*016*/0x80,/*017*/0x80,/*018*/0x80,/*019*/0x80,
|
||||
/*020*/0x80,/*021*/0x80,/*022*/0x80,/*023*/0x80,/*024*/0x80,/*025*/0x80,/*026*/0x80,/*027*/0x80,/*028*/0x80,/*029*/0x80,/*030*/0x80,/*031*/0x80,/*032*/0x80,/*033*/0x80,/*034*/0x80,/*035*/0x80,/*036*/0x80,/*037*/0x80,/*038*/0x80,/*039*/0x80,
|
||||
/*040*/0x80,/*041*/0x80,/*042*/0x80,/*043*/0x3e,/*044*/0x80,/*045*/0x80,/*046*/0x80,/*047*/0x3f,/*048*/0x34,/*049*/0x35,/*050*/0x36,/*051*/0x37,/*052*/0x38,/*053*/0x39,/*054*/0x3a,/*055*/0x3b,/*056*/0x3c,/*057*/0x3d,/*058*/0x80,/*059*/0x80,
|
||||
/*060*/0x80,/*061*/0x00,/*062*/0x80,/*063*/0x80,/*064*/0x80,/*065*/0x00,/*066*/0x01,/*067*/0x02,/*068*/0x03,/*069*/0x04,/*070*/0x05,/*071*/0x06,/*072*/0x07,/*073*/0x08,/*074*/0x09,/*075*/0x0a,/*076*/0x0b,/*077*/0x0c,/*078*/0x0d,/*079*/0x0e,
|
||||
/*080*/0x0f,/*081*/0x10,/*082*/0x11,/*083*/0x12,/*084*/0x13,/*085*/0x14,/*086*/0x15,/*087*/0x16,/*088*/0x17,/*089*/0x18,/*090*/0x19,/*091*/0x80,/*092*/0x80,/*093*/0x80,/*094*/0x80,/*095*/0x80,/*096*/0x80,/*097*/0x1a,/*098*/0x1b,/*099*/0x1c,
|
||||
/*100*/0x1d,/*101*/0x1e,/*102*/0x1f,/*103*/0x20,/*104*/0x21,/*105*/0x22,/*106*/0x23,/*107*/0x24,/*108*/0x25,/*109*/0x26,/*110*/0x27,/*111*/0x28,/*112*/0x29,/*113*/0x2a,/*114*/0x2b,/*115*/0x2c,/*116*/0x2d,/*117*/0x2e,/*118*/0x2f,/*119*/0x30,
|
||||
/*120*/0x31,/*121*/0x32,/*122*/0x33,/*123*/0x80,/*124*/0x80,/*125*/0x80,/*126*/0x80,/*127*/0x80,/*128*/0x80,/*129*/0x80,/*130*/0x80,/*131*/0x80,/*132*/0x80,/*133*/0x80,/*134*/0x80,/*135*/0x80,/*136*/0x80,/*137*/0x80,/*138*/0x80,/*139*/0x80,
|
||||
/*140*/0x80,/*141*/0x80,/*142*/0x80,/*143*/0x80,/*144*/0x80,/*145*/0x80,/*146*/0x80,/*147*/0x80,/*148*/0x80,/*149*/0x80,/*150*/0x80,/*151*/0x80,/*152*/0x80,/*153*/0x80,/*154*/0x80,/*155*/0x80,/*156*/0x80,/*157*/0x80,/*158*/0x80,/*159*/0x80,
|
||||
/*160*/0x80,/*161*/0x80,/*162*/0x80,/*163*/0x80,/*164*/0x80,/*165*/0x80,/*166*/0x80,/*167*/0x80,/*168*/0x80,/*169*/0x80,/*170*/0x80,/*171*/0x80,/*172*/0x80,/*173*/0x80,/*174*/0x80,/*175*/0x80,/*176*/0x80,/*177*/0x80,/*178*/0x80,/*179*/0x80,
|
||||
/*180*/0x80,/*181*/0x80,/*182*/0x80,/*183*/0x80,/*184*/0x80,/*185*/0x80,/*186*/0x80,/*187*/0x80,/*188*/0x80,/*189*/0x80,/*190*/0x80,/*191*/0x80,/*192*/0x80,/*193*/0x80,/*194*/0x80,/*195*/0x80,/*196*/0x80,/*197*/0x80,/*198*/0x80,/*199*/0x80,
|
||||
/*200*/0x80,/*201*/0x80,/*202*/0x80,/*203*/0x80,/*204*/0x80,/*205*/0x80,/*206*/0x80,/*207*/0x80,/*208*/0x80,/*209*/0x80,/*210*/0x80,/*211*/0x80,/*212*/0x80,/*213*/0x80,/*214*/0x80,/*215*/0x80,/*216*/0x80,/*217*/0x80,/*218*/0x80,/*219*/0x80,
|
||||
/*220*/0x80,/*221*/0x80,/*222*/0x80,/*223*/0x80,/*224*/0x80,/*225*/0x80,/*226*/0x80,/*227*/0x80,/*228*/0x80,/*229*/0x80,/*230*/0x80,/*231*/0x80,/*232*/0x80,/*233*/0x80,/*234*/0x80,/*235*/0x80,/*236*/0x80,/*237*/0x80,/*238*/0x80,/*239*/0x80,
|
||||
/*240*/0x80,/*241*/0x80,/*242*/0x80,/*243*/0x80,/*244*/0x80,/*245*/0x80,/*246*/0x80,/*247*/0x80,/*248*/0x80,/*249*/0x80,/*250*/0x80,/*251*/0x80,/*252*/0x80,/*253*/0x80,/*254*/0x80,/*255*/0x00,
|
||||
};
|
||||
|
||||
size_t base64c_encoding_length(size_t len) {
|
||||
size_t olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
|
||||
olen++; /* nul termination */
|
||||
if (olen < len)
|
||||
return 0; /* integer overflow */
|
||||
return olen;
|
||||
}
|
||||
|
||||
size_t base64c_encode(const unsigned char *src, size_t len,
|
||||
unsigned char* out, const size_t out_len)
|
||||
{
|
||||
unsigned char *pos;
|
||||
const unsigned char *end, *in;
|
||||
const unsigned char *out_end = out + out_len;
|
||||
|
||||
end = src + len;
|
||||
in = src;
|
||||
pos = out;
|
||||
|
||||
if (out_len < base64c_encoding_length(len)) { return -1; }
|
||||
|
||||
while (end - in >= 3 ) {
|
||||
*pos++ = base64c_table[in[0] >> 2];
|
||||
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
|
||||
*pos++ = base64c_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
|
||||
*pos++ = base64c_table[in[2] & 0x3f];
|
||||
in += 3;
|
||||
}
|
||||
|
||||
if (end - in) {
|
||||
*pos++ = base64c_table[in[0] >> 2];
|
||||
|
||||
if (end - in == 1) {
|
||||
*pos++ = base64c_table[(in[0] & 0x03) << 4];
|
||||
*pos++ = '=';
|
||||
} else {
|
||||
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
|
||||
*pos++ = base64c_table[(in[1] & 0x0f) << 2];
|
||||
}
|
||||
*pos++ = '=';
|
||||
}
|
||||
|
||||
*pos = '\0';
|
||||
|
||||
return out_len - (out_end-pos);
|
||||
}
|
||||
|
||||
size_t base64c_decoding_length(size_t inlen) {
|
||||
return inlen / 4 * 3;
|
||||
}
|
||||
|
||||
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len)
|
||||
{
|
||||
if (out == NULL) { return 0; }
|
||||
if (out_len <= 0) { return 0; }
|
||||
|
||||
unsigned char *pos, block[4], tmp;
|
||||
size_t i, count;
|
||||
int pad = 0;
|
||||
|
||||
if (len == 0 ){
|
||||
*out = '\0';
|
||||
return 1;
|
||||
}
|
||||
if (len % 4) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
pos = out;
|
||||
count = 0;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (src[i] == '=') { pad++; }
|
||||
tmp = base64c_dtable[src[i]];
|
||||
|
||||
if (tmp == 0x80) { return -1; }
|
||||
|
||||
block[count] = tmp;
|
||||
count++;
|
||||
if (count == 4) {
|
||||
switch (pad) {
|
||||
case 0:
|
||||
if ((pos - out) + 3 > out_len) {
|
||||
return -1;
|
||||
}
|
||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
||||
*pos++ = (block[1] << 4) | (block[2] >> 2);
|
||||
*pos++ = (block[2] << 6) | block[3];
|
||||
break;
|
||||
case 1:
|
||||
if ((pos - out) + 2 > out_len || i + 1 > len) {
|
||||
return -1;
|
||||
}
|
||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
||||
*pos++ = (block[1] << 4) | (block[2] >> 2);
|
||||
break;
|
||||
case 2:
|
||||
if ((pos - out) + 1 > out_len || i + 1 > len) {
|
||||
return -1;
|
||||
}
|
||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return pos - out;
|
||||
}
|
16
deps/base64c/test/Makefile.am
vendored
16
deps/base64c/test/Makefile.am
vendored
@ -1,16 +0,0 @@
|
||||
CFLAGS = --pedantic -Wall -std=c99 -g -ggdb
|
||||
LDFLAGS =
|
||||
|
||||
bin_PROGRAMS = test001 test002 test003 test004 \
|
||||
test005 test006 test007 test008 \
|
||||
gen
|
||||
|
||||
test001_SOURCES = test001.c ../src/base64c.c
|
||||
test002_SOURCES = test002.c ../src/base64c.c
|
||||
test003_SOURCES = test003.c ../src/base64c.c
|
||||
test004_SOURCES = test004.c ../src/base64c.c
|
||||
test005_SOURCES = test005.c ../src/base64c.c
|
||||
test006_SOURCES = test006.c ../src/base64c.c
|
||||
test007_SOURCES = test007.c ../src/base64c.c
|
||||
test008_SOURCES = test008.c ../src/base64c.c
|
||||
gen_SOURCES = gen.c
|
22
deps/base64c/test/gen.c
vendored
22
deps/base64c/test/gen.c
vendored
@ -1,22 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const unsigned char base64_table[65] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
int main() {
|
||||
unsigned char out[256];
|
||||
|
||||
memset(out, 0x80, 255);
|
||||
for (int i = 0; i < 64; i++) {
|
||||
out[base64_table[i]] = i;
|
||||
}
|
||||
out['='] = 0;
|
||||
|
||||
printf("static const unsigned char base64c_dtable[256] = {");
|
||||
for (int i = 0; i < 256; i++) {
|
||||
if (i% 20==0) { printf("\n"); }
|
||||
printf("/*%03d*/0x%02x,", i, out[i]);
|
||||
}
|
||||
printf("\n};");
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user