Compare commits

..

No commits in common. "main" and "v0.0.26" have entirely different histories.

45 changed files with 1645 additions and 2534 deletions

View File

@ -24,14 +24,15 @@ jobs:
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
with: with:
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264' packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11 gcc-aarch64-linux-gnu - run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs - run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
- run: docker build . - run: docker build .
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
path: | path: out/TildeFriends-release.fdroid.apk
out/TildeFriends-release.fdroid.apk - uses: actions/upload-artifact@v3
out/winrelease/tildefriends.standalone.exe with:
out/tildefriends-x86_64.AppImage path: out/winrelease/tildefriends.exe
out/release/tildefriends.standalone - uses: actions/upload-artifact@v3
out/armrelease/tildefriends.standalone with:
path: out/tildefriends-x86_64.AppImage

View File

@ -16,8 +16,8 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker. ## LD := Linker.
## ANDROID_SDK := Path to the Android SDK. ## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 32 VERSION_CODE := 31
VERSION_NUMBER := 0.0.27-wip VERSION_NUMBER := 0.0.26
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip
@ -34,7 +34,6 @@ ANDROID_SDK ?= ~/Android/Sdk
BUNDLETOOL = out/bundletool.jar BUNDLETOOL = out/bundletool.jar
HAVE_WIN := 0 HAVE_WIN := 0
HAVE_CROSS_AARCH64 := 0
export SOURCE_DATE_EPOCH=1 export SOURCE_DATE_EPOCH=1
export TZ=UTC export TZ=UTC
@ -46,9 +45,6 @@ BUILD_TYPES := debug release
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0) HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0) HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0) HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
ifneq ($(UNAME_M),aarch64)
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1,0)
endif
else ifeq ($(UNAME_S),Haiku) else ifeq ($(UNAME_S),Haiku)
BUILD_TYPES := debug release BUILD_TYPES := debug release
CFLAGS += -Dstatic_assert=_Static_assert CFLAGS += -Dstatic_assert=_Static_assert
@ -127,14 +123,6 @@ WINDOWS_TARGETS := \
out/winrelease/tildefriends.exe out/winrelease/tildefriends.exe
ifeq ($(HAVE_WIN),1) ifeq ($(HAVE_WIN),1)
BUILD_TYPES += windebug winrelease BUILD_TYPES += windebug winrelease
all: out/windebug/tildefriends.standalone.exe out/winrelease/tildefriends.standalone.exe
endif
AARCH64_TARGETS := \
out/armdebug/tildefriends \
out/armrelease/tildefriends
ifeq ($(HAVE_CROSS_AARCH64),1)
BUILD_TYPES += armdebug armrelease
endif endif
LINUX_TARGETS := \ LINUX_TARGETS := \
@ -161,9 +149,6 @@ all: $(IOS_APPS) \
out/tildefriends-iossimdebug.app/tildefriends \ out/tildefriends-iossimdebug.app/tildefriends \
out/tildefriends-iossimrelease.app/tildefriends out/tildefriends-iossimrelease.app/tildefriends
endif endif
ifeq ($(HAVE_CROSS_AARCH64),1)
all: out/armrelease/tildefriends.standalone
endif
DEBUG_TARGETS := \ DEBUG_TARGETS := \
out/debug/tildefriends \ out/debug/tildefriends \
@ -174,8 +159,7 @@ DEBUG_TARGETS := \
out/androiddebug/tildefriends \ out/androiddebug/tildefriends \
out/androiddebug-armv7a/tildefriends \ out/androiddebug-armv7a/tildefriends \
out/androiddebug-x86_64/tildefriends \ out/androiddebug-x86_64/tildefriends \
out/androiddebug-x86/tildefriends \ out/androiddebug-x86/tildefriends
out/armdebug/tildefriends
RELEASE_TARGETS := \ RELEASE_TARGETS := \
out/release/tildefriends \ out/release/tildefriends \
out/winrelease/tildefriends.exe \ out/winrelease/tildefriends.exe \
@ -185,8 +169,7 @@ RELEASE_TARGETS := \
out/androidrelease/tildefriends \ out/androidrelease/tildefriends \
out/androidrelease-armv7a/tildefriends \ out/androidrelease-armv7a/tildefriends \
out/androidrelease-x86_64/tildefriends \ out/androidrelease-x86_64/tildefriends \
out/androidrelease-x86/tildefriends \ out/androidrelease-x86/tildefriends
out/armrelease/tildefriends
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS) ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS)) ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS)) NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
@ -209,14 +192,11 @@ $(ANDROID_TARGETS): CFLAGS += \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC $(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og $(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \ $(RELEASE_TARGETS): CFLAGS += \
-DNDEBUG \ -DNDEBUG \
-flto -flto
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz $(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(ANDROID_RELEASE_TARGETS): LDFLAGS += -Oz
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -Os
$(NONANDROID_RELEASE_TARGETS): LDFLAGS += -Os
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32 $(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
$(WINDOWS_TARGETS): AS = $(CC) $(WINDOWS_TARGETS): AS = $(CC)
$(WINDOWS_TARGETS): CFLAGS += \ $(WINDOWS_TARGETS): CFLAGS += \
@ -228,10 +208,6 @@ $(WINDOWS_TARGETS): LDFLAGS += \
-static \ -static \
-lm \ -lm \
-Ldeps/openssl/mingw64/usr/local/lib -Ldeps/openssl/mingw64/usr/local/lib
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
$(AARCH64_TARGETS): AS = $(CC)
$(AARCH64_TARGETS): CFLAGS += -Ideps/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Ldeps/openssl/Linux/aarch64/usr/local/lib
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
$(MACOS_TARGETS): CC = xcrun clang $(MACOS_TARGETS): CC = xcrun clang
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path) $(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
@ -239,8 +215,7 @@ $(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch a
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path) $(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64 $(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
else ifeq ($(UNAME_S),Linux) else ifeq ($(UNAME_S),Linux)
$(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64 $(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk
$(IOS_TARGETS): LDFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang $(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
endif endif
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android $(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
@ -263,23 +238,14 @@ $(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/inc
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib $(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type $(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections $(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 $(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 $(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
ifeq ($(UNAME_S),Darwin)
$(IOS_TARGETS): CFLAGS += -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
else
$(IOS_TARGETS): CFLAGS += -Ideps/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
endif
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include $(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
$(LINUX_TARGETS) $(MACOS_TARGETS): CFLAGS += -Ideps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(LINUX_TARGETS) $(MACOS_TARGETS): LDFLAGS += -Ldeps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux) ifeq ($(UNAME_S),Linux)
all: appimage out/release/tildefriends.standalone all: appimage
endif endif
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
@ -294,7 +260,7 @@ endif
get_objs = \ get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \ $(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \ $(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,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \ $(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \ $(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
@ -759,7 +725,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \ LDFLAGS += \
-pthread \ -pthread \
-lm -lm
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS): LDFLAGS += \ $(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-lssl \ -lssl \
-lcrypto -lcrypto
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
@ -799,8 +765,6 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
## ##
debug: ## Build a debug executable for the current platform. debug: ## Build a debug executable for the current platform.
release: ## Build a release executable for the current platform. release: ## Build a release executable for the current platform.
armdebug: ## Cross-compile aarch64 debug on Linux.
armrelease: ## Cross-compile aarch64 release on Linux.
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine. all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
unix: debug release ## Build all UNIX targets. unix: debug release ## Build all UNIX targets.
win: windebug winrelease ## Build all Windows targets. win: windebug winrelease ## Build all Windows targets.
@ -1092,7 +1056,6 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@cp -v $< $@ @cp -v $< $@
out/data.zip: $(RAW_FILES) out/data.zip: $(RAW_FILES)
@echo [zip] $@
@zip -u $@ -q -9 $(RAW_FILES) @zip -u $@ -q -9 $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
@ -1143,42 +1106,6 @@ $(ANDROID_DEPS):
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android +@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS) $(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(UNAME_S),Linux)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS=-flto tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
ifeq ($(HAVE_CROSS_AARCH64),1)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu- -flto" BUILD_TARGET=aarch64 tools/ssl-local
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_IOS),1)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=deps/ios_toolchain/target/bin:$$PATH \
BUILD_TARGET=ios64-cross \
SSL_TARGET=ios64-cross \
CROSS_COMPILE=../../deps/ios_toolchain/target/bin/arm-apple-darwin11- \
CROSS_TOP=../../deps/ios_toolchain/target \
CROSS_SDK=iPhoneOS18.2.sdk \
CC=clang \
OPTIONS=-miphoneos-version-min=9.0 \
tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
endif
ifeq ($(UNAME_S),Darwin)
LOCAL_DEPS := deps/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS=-flto tools/ssl-local
$(filter $(BUILD_DIR)/macosdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/macosrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_WIN),1) ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS): $(WINDOWS_DEPS):
@ -1271,7 +1198,7 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
.PHONY: tarball .PHONY: tarball
dist: ## Build versions of all distributables for release. dist: ## Build versions of all distributables for release.
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone $(if $(HAVE_CROSS_AARCH64), out/armrelease/tildefriends.standalone) dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball
@mkdir -p dist/ @mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz" @echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz @cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@ -1289,10 +1216,6 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk @cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage" @echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage @cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
@echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)
@test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"
@test $(HAVE_CROSS_AARCH64) && cp out/armrelease/tildefriends.standalone dist/tildefriends-linux-aarch64-$(VERSION_NUMBER)
.PHONY: dist .PHONY: dist
dist-test: dist ## Exercise some built distributable files, making sure they work as intended. dist-test: dist ## Exercise some built distributable files, making sure they work as intended.
@ -1335,7 +1258,7 @@ help: ## Display this help message.
/^##/ { sub(/^## ?/, ""); print $$0 } \ /^##/ { sub(/^## ?/, ""); print $$0 } \
/^[[:alnum:]-]+:.*##/ { \ /^[[:alnum:]-]+:.*##/ { \
sub(/:.*##\s?/, ":"); \ sub(/:.*##\s?/, ":"); \
printf " %s%-21s%s %s%s%s\n", G, $$1, R, O, $$2, R \ printf " %s%-20s%s %s%s%s\n", G, $$1, R, O, $$2, R \
} \ } \
' < $(filter-out %.d,$(MAKEFILE_LIST)) ' < $(filter-out %.d,$(MAKEFILE_LIST))
@echo "" # Blank line. @echo "" # Blank line.

View File

@ -1,5 +1,4 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "➡️", "emoji": "➡️"
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
} }

View File

@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
result.blocking = result.blocking || {}; result.blocking = result.blocking || {};
let contacts = await query( let contacts = await query(
` `
SELECT json(content) AS content FROM messages SELECT content FROM messages
WHERE author = ? AND WHERE author = ? AND
rowid > ? AND rowid > ? AND
rowid <= ? AND rowid <= ? AND
@ -189,6 +189,50 @@ async function fetch_about(db, ids, users) {
return Object.assign({}, users); return Object.assign({}, users);
} }
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
let o = await db.get(id + ':about');
const k_version = 4;
let f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlAsync(
'SELECT ' +
' sequence, ' +
' content ' +
'FROM messages ' +
'WHERE ' +
' author = ?1 AND ' +
' sequence > ?2 AND ' +
" json_extract(content, '$.type') = 'about' AND " +
" json_extract(content, '$.about') = ?1 " +
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
'ORDER BY sequence',
[id, f.sequence],
function (row) {
f.sequence = row.sequence;
if (row.content) {
let about = {};
try {
about = JSON.parse(row.content);
} catch {}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
}
);
let j = JSON.stringify(f);
if (o != j) {
await db.set(id + ':about', j);
}
g_about_cache[id] = f.about;
return f.about;
}
async function getSize(db, id) { async function getSize(db, id) {
let size = 0; let size = 0;
await ssb.sqlAsync( await ssb.sqlAsync(

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🐌",
"previous": "&jbL9Ab+XdvWnZbb50yimceFHR7XFDfBSWv9/XrbZ82I=.sha256" "previous": "&q/1uGp0jMvsYGW7Gj8E33kf6UFo/uNYDXg3zo1sVKQg=.sha256"
} }

View File

@ -21,6 +21,9 @@ tfrpc.register(async function createIdentity() {
tfrpc.register(async function getServerIdentity() { tfrpc.register(async function getServerIdentity() {
return ssb.getServerIdentity(); return ssb.getServerIdentity();
}); });
tfrpc.register(async function setServerFollowingMe(id, following) {
return ssb.setServerFollowingMe(id, following);
});
tfrpc.register(async function getIdentities() { tfrpc.register(async function getIdentities() {
return ssb.getIdentities(); return ssb.getIdentities();
}); });
@ -103,10 +106,6 @@ tfrpc.register(async function getActiveIdentity() {
tfrpc.register(async function sync() { tfrpc.register(async function sync() {
return await ssb.sync(); return await ssb.sync();
}); });
tfrpc.register(async function url() {
return core.url;
});
core.register('onBroadcastsChanged', async function () { core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
}); });

View File

@ -7,7 +7,7 @@ function textNode(text) {
function linkNode(text, link) { function linkNode(text, link) {
const linkNode = new commonmark.Node('link', undefined); const linkNode = new commonmark.Node('link', undefined);
if (link.startsWith('#')) { if (link.startsWith('#')) {
linkNode.destination = `#${encodeURIComponent(link)}`; linkNode.destination = `#${encodeURIComponent('#' + link)}`;
} else { } else {
linkNode.destination = link; linkNode.destination = link;
} }

View File

@ -7,6 +7,7 @@ class TfElement extends LitElement {
return { return {
whoami: {type: String}, whoami: {type: String},
hash: {type: String}, hash: {type: String},
unread: {type: Array},
tab: {type: String}, tab: {type: String},
broadcasts: {type: Array}, broadcasts: {type: Array},
connections: {type: Array}, connections: {type: Array},
@ -18,8 +19,6 @@ class TfElement extends LitElement {
channels: {type: Array}, channels: {type: Array},
channels_unread: {type: Object}, channels_unread: {type: Object},
channels_latest: {type: Object}, channels_latest: {type: Object},
guest: {type: Boolean},
url: {type: String},
}; };
} }
@ -29,6 +28,7 @@ class TfElement extends LitElement {
super(); super();
let self = this; let self = this;
this.hash = '#'; this.hash = '#';
this.unread = [];
this.tab = 'news'; this.tab = 'news';
this.broadcasts = []; this.broadcasts = [];
this.connections = []; this.connections = [];
@ -68,9 +68,7 @@ class TfElement extends LitElement {
async initial_load() { async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || []; let ids = (await tfrpc.rpc.getIdentities()) || [];
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
this.ids = ids; this.ids = ids;
await this.load_channels(); await this.load_channels();
} }
@ -114,24 +112,26 @@ class TfElement extends LitElement {
keydown(event) { keydown(event) {
if (event.altKey && event.key == 'ArrowUp') { if (event.altKey && event.key == 'ArrowUp') {
this.next_channel(-1); this.next_channel(1);
event.preventDefault(); event.preventDefault();
} else if (event.altKey && event.key == 'ArrowDown') { } else if (event.altKey && event.key == 'ArrowDown') {
this.next_channel(1); this.next_channel(-1);
event.preventDefault(); event.preventDefault();
} }
} }
next_channel(delta) { next_channel(delta) {
let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)]; let channel_names = ['', '@'].concat(this.channels);
let index = channel_names.indexOf(this.hash.substring(1)); let index = channel_names.indexOf(this.hash.substring(1));
index = index != -1 ? index + delta : 0; if (index != -1) {
tfrpc.rpc.setHash( index += delta;
'#' + this.set_hash(
encodeURIComponent( '#' +
channel_names[(index + channel_names.length) % channel_names.length] encodeURIComponent(
) channel_names[(index + channel_names.length) % channel_names.length]
); )
);
}
} }
set_hash(hash) { set_hash(hash) {
@ -150,7 +150,6 @@ class TfElement extends LitElement {
async fetch_about(ids, users) { async fetch_about(ids, users) {
const k_cache_version = 1; const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about'); let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache;
cache = cache ? JSON.parse(cache) : {}; cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) { if (cache.version !== k_cache_version) {
cache = { cache = {
@ -216,13 +215,7 @@ class TfElement extends LitElement {
} }
} }
cache.last_row_id = max_row_id; cache.last_row_id = max_row_id;
let new_cache = JSON.stringify(cache); await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
if (new_cache !== original_cache) {
let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000);
});
}
users = users || {}; users = users || {};
for (let id of Object.keys(cache.about)) { for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(users[id] || {}, cache.about[id]); users[id] = Object.assign(users[id] || {}, cache.about[id]);
@ -241,13 +234,17 @@ class TfElement extends LitElement {
[JSON.stringify(this.following), id] [JSON.stringify(this.following), id]
); );
for (let message of messages) { for (let message of messages) {
if ( if (message.author == this.whoami) {
message.author == this.whoami && let content = JSON.parse(message.content);
JSON.parse(message.content)?.type == 'channel' if (content?.type == 'channel') {
) { this.load_channels();
this.load_channels(); }
} }
} }
if (messages && messages.length) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
}
this.schedule_load_channels_latest(); this.schedule_load_channels_latest();
} }
@ -274,74 +271,31 @@ class TfElement extends LitElement {
} }
async get_latest_private(following) { async get_latest_private(following) {
const k_version = 1;
// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
let cache = JSON.parse(
(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
);
if (cache.version !== k_version) {
cache = {
version: k_version,
messages: [],
range: [],
};
}
let latest = ( let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages') await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest; )[0].latest;
let ranges = []; const k_chunk_count = 256;
const k_chunk_size = 512; while (latest - k_chunk_count >= 0) {
if (cache.range.length) {
for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
ranges.push([
Math.max(i - k_chunk_size, 0),
Math.min(cache.range[0], i + k_chunk_size),
false,
]);
}
} else {
for (let i = 0; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
}
console.log(cache);
for (let range of ranges) {
let messages = await tfrpc.rpc.query( let messages = await tfrpc.rpc.query(
` `
SELECT messages.rowid, messages.id, json(content) AS content SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE WHERE
messages.rowid > ?1 AND messages.rowid > ?2 AND
messages.rowid <= ?2 AND messages.rowid <= ?3 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY sequence DESC ORDER BY sequence DESC
`, `,
[range[0], range[1]] [JSON.stringify(following), latest - k_chunk_count, latest]
); );
messages = (await this.decrypt(messages)).filter((x) => x.decrypted); messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
if (messages.length) { if (messages.length) {
cache.latest = Math.max( return Math.max(...messages.map((x) => x.rowid));
cache.latest ?? 0,
...messages.map((x) => x.rowid)
);
if (range[2]) {
cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
} else {
cache.messages = [...messages.map((x) => x.id), ...cache.messages];
}
} }
cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]); latest -= k_chunk_count;
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
await tfrpc.rpc.databaseSet(
`private:${this.whoami}`,
JSON.stringify(cache)
);
} }
console.log(cache); return -1;
return cache.latest;
} }
async load_channels_latest(following) { async load_channels_latest(following) {
@ -354,29 +308,20 @@ class TfElement extends LitElement {
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE WHERE messages.content ->> 'type' = 'post' AND messages.content ->> 'root' IS NULL
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
GROUP by channel GROUP by channel
UNION UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
UNION UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3) SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`, `,
[ [
JSON.stringify(this.channels), JSON.stringify(this.channels),
JSON.stringify(following), JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"', '"' + this.whoami.replace('"', '""') + '"',
this.whoami,
] ]
); );
this.channels_latest = Object.fromEntries( this.channels_latest = Object.fromEntries(
@ -404,16 +349,14 @@ class TfElement extends LitElement {
schedule_load_channels_latest() { schedule_load_channels_latest() {
if (!this.loading_channels_latest) { if (!this.loading_channels_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load_channels_latest(this.following); this.load_channels_latest(this.following);
} else if (!this.loading_channels_latest_scheduled) { } else if (!this.loading_channels_latest_scheduled) {
this.loading_channels_latest_scheduled++; this.loading_channels_latest_scheduled++;
setTimeout(this._schedule_load_channels_latest_timer.bind(this), 5000); setTimeout(this._schedule_load_channels_latest_timer, 5000);
} }
} }
async load() { async load() {
let start_time = new Date();
let whoami = this.whoami; let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2); let following = await tfrpc.rpc.following([whoami], 2);
let users = {}; let users = {};
@ -427,10 +370,11 @@ class TfElement extends LitElement {
}; };
by_count.push({count: v.of, id: id}); by_count.push({count: v.of, id: id});
} }
this.load_channels_latest(Object.keys(following)); let channels_latest = this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse( this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}' (await tfrpc.rpc.databaseGet('unread')) ?? '{}'
); );
let start_time = new Date();
users = await this.fetch_about(Object.keys(following).sort(), users); users = await this.fetch_about(Object.keys(following).sort(), users);
console.log( console.log(
'about took', 'about took',
@ -439,11 +383,11 @@ class TfElement extends LitElement {
Object.keys(users).length, Object.keys(users).length,
'users' 'users'
); );
start_time = new Date();
await channels_latest;
this.following = Object.keys(following); this.following = Object.keys(following);
this.users = users; this.users = users;
console.log( console.log(`load finished ${whoami} => ${this.whoami}`);
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
this.whoami = whoami; this.whoami = whoami;
this.loaded = whoami; this.loaded = whoami;
} }
@ -491,12 +435,13 @@ class TfElement extends LitElement {
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
hash=${this.hash} hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
?loading=${this.loading} ?loading=${this.loading}
.channels=${this.channels} .channels=${this.channels}
.channels_latest=${this.channels_latest} .channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread} .channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread} @channelsetunread=${this.channel_set_unread}
.connections=${this.connections}
></tf-tab-news> ></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {
@ -567,7 +512,7 @@ class TfElement extends LitElement {
let tabs = html` let tabs = html`
<div <div
class="w3-bar w3-theme-l1" class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10" style="position: sticky; top: 0; z-index: 10"
> >
<button <button
class="w3-bar-item w3-button w3-circle w3-ripple" class="w3-bar-item w3-button w3-circle w3-ripple"
@ -593,36 +538,22 @@ class TfElement extends LitElement {
)} )}
</div> </div>
`; `;
let contents = this.guest let contents = !this.loaded
? html`<div ? this.loading
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
>
<p>🦀 Must be logged in to Tilde Friends to scuttle here. 🦀</p>
<footer class="w3-center">
<a
class="w3-button w3-theme-d1"
href=${`/login?return=${encodeURIComponent(this.url)}`}
>Login</a
>
</footer>
</div>`
: !this.loaded || this.loading
? html`<div ? html`<div
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge" class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
> >
<span class="w3-spin" style="display: inline-block">🦀</span> Loading...
Loading... </div>
</div>` ${this.render_tab()}`
: this.render_tab(); : html`<div>Select or create an identity.</div>`
: this.render_tab();
return html` return html`
<div <div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark" class="w3-theme-dark"
> >
<div style="flex: 0 0">${tabs}</div> ${tabs} ${contents}
<div style="flex: 1 1; overflow: auto; contain: layout">
${contents}
</div>
</div> </div>
`; `;
} }

View File

@ -15,7 +15,6 @@ class TfComposeElement extends LitElement {
drafts: {type: Object}, drafts: {type: Object},
author: {type: String}, author: {type: String},
channel: {type: String}, channel: {type: String},
new_thread: {type: Boolean},
}; };
} }
@ -29,7 +28,6 @@ class TfComposeElement extends LitElement {
this.apps = undefined; this.apps = undefined;
this.drafts = {}; this.drafts = {};
this.author = undefined; this.author = undefined;
this.new_thread = false;
} }
process_text(text) { process_text(text) {
@ -202,23 +200,9 @@ class TfComposeElement extends LitElement {
channel: this.channel, channel: this.channel,
}; };
if (this.root || this.branch) { if (this.root || this.branch) {
message.root = this.new_thread ? (this.branch ?? this.root) : this.root; message.root = this.root;
message.branch = this.branch; message.branch = this.branch;
} }
let reply = Object.fromEntries(
(
await tfrpc.rpc.query(
`
SELECT messages.id, messages.author FROM messages
JOIN json_each(?) AS refs ON messages.id = refs.value
`,
[JSON.stringify([this.root, this.branch])]
)
).map((row) => [row.id, row.author])
);
if (Object.keys(reply).length) {
message.reply = reply;
}
if (Object.values(draft.mentions || {}).length) { if (Object.values(draft.mentions || {}).length) {
message.mentions = Object.values(draft.mentions); message.mentions = Object.values(draft.mentions);
} }
@ -485,20 +469,6 @@ class TfComposeElement extends LitElement {
} }
} }
render_new_thread() {
let self = this;
if (
this.root !== undefined &&
this.branch !== undefined &&
this.root != this.branch
) {
return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
<label for="new_thread">New Thread</label>
`;
}
}
get_draft() { get_draft() {
return this.drafts[this.branch || ''] || {}; return this.drafts[this.branch || ''] || {};
} }
@ -563,24 +533,14 @@ class TfComposeElement extends LitElement {
🔐 🔐
</button>`; </button>`;
let result = html` let result = html`
<style>
.w3-input:empty::before {
content: attr(placeholder);
}
.w3-input:empty:focus::before {
content: '';
}
</style>
<div <div
class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom" class="w3-card-4 w3-theme-d4 w3-padding-small"
style="box-sizing: border-box" style="box-sizing: border-box"
> >
<header class="w3-container"> ${this.channel !== undefined
${this.channel !== undefined ? html`<p>To #${this.channel}:</p>`
? html`<p>To #${this.channel}:</p>` : undefined}
: undefined} ${this.render_encrypt()}
${this.render_encrypt()}
</header>
<div class="w3-container w3-padding-small"> <div class="w3-container w3-padding-small">
<div class="w3-half"> <div class="w3-half">
<span <span
@ -594,32 +554,25 @@ class TfComposeElement extends LitElement {
.innerText=${live(draft.text ?? '')} .innerText=${live(draft.text ?? '')}
></span> ></span>
</div> </div>
<div class="w3-half"> <div class="w3-half w3-padding">
${content_warning} ${content_warning}
<p id="preview"></p> <div id="preview"></div>
</div> </div>
</div> </div>
${Object.values(draft.mentions || {}).map((x) => ${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x) self.render_mention(x)
)} )}
<footer class="w3-container"> ${this.render_attach_app()} ${this.render_content_warning()}
${this.render_attach_app()} ${this.render_content_warning()} <button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
${this.render_new_thread()} Submit
<button </button>
class="w3-button w3-theme-d1" <button class="w3-button w3-theme-d1" @click=${this.attach}>
id="submit" Attach
@click=${this.submit} </button>
> ${this.render_attach_app_button()} ${encrypt}
Submit <button class="w3-button w3-theme-d1" @click=${this.discard}>
</button> Discard
<button class="w3-button w3-theme-d1" @click=${this.attach}> </button>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-theme-d1" @click=${this.discard}>
Discard
</button>
</footer>
</div> </div>
`; `;
return result; return result;

View File

@ -97,13 +97,6 @@ class TfMessageElement extends LitElement {
} }
} }
render_json(value) {
let json = JSON.stringify(value, null, 2);
return html`
<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
`;
}
render_raw() { render_raw() {
let raw = { let raw = {
id: this.message?.id, id: this.message?.id,
@ -115,7 +108,9 @@ class TfMessageElement extends LitElement {
content: this.message?.content, content: this.message?.content,
signature: this.message?.signature, signature: this.message?.signature,
}; };
return this.render_json(raw); return html`<div style="white-space: pre-wrap">
${JSON.stringify(raw, null, 2)}
</div>`;
} }
vote(emoji) { vote(emoji) {
@ -195,7 +190,7 @@ class TfMessageElement extends LitElement {
render_mention(mention) { render_mention(mention) {
if (!mention?.link || typeof mention.link != 'string') { if (!mention?.link || typeof mention.link != 'string') {
return this.render_json(mention); return html` <pre>${JSON.stringify(mention)}</pre>`;
} else if ( } else if (
mention?.link?.startsWith('&') && mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/') mention?.type?.startsWith('image/')
@ -246,17 +241,16 @@ class TfMessageElement extends LitElement {
) { ) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`; return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else { } else {
return this.render_json(mention); return html` <pre style="white-space: pre-wrap">
${JSON.stringify(mention, null, 2)}</pre
>`;
} }
} }
render_mentions() { render_mentions() {
let mentions = this.message?.content?.mentions || []; let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter( mentions = mentions.filter(
(x) => (x) => this.message?.content?.text?.indexOf(x.link) === -1
this.message?.content?.text?.indexOf(
typeof x === 'string' ? x : x.link
) === -1
); );
if (mentions.length) { if (mentions.length) {
let self = this; let self = this;
@ -326,8 +320,6 @@ class TfMessageElement extends LitElement {
></tf-message>` ></tf-message>`
)}`; )}`;
} }
} else {
return undefined;
} }
} }
@ -363,38 +355,31 @@ class TfMessageElement extends LitElement {
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`); return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
} }
class_background() { render() {
return this.message?.decrypted
? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4';
}
get_content() {
let content = this.message?.content; let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') { if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted; content = this.message.decrypted;
} }
return content; let class_background = this.message?.decrypted
} ? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
render_raw_button() { ? 'w3-theme-d2'
let content = this.get_content(); : 'w3-theme-d4';
let self = this;
let raw_button; let raw_button;
switch (this.format) { switch (this.format) {
case 'raw': case 'raw':
if (content?.type == 'post' || content?.type == 'blog') { if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'md')} @click=${() => (self.format = 'md')}
> >
Markdown Markdown
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'message')} @click=${() => (self.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -403,7 +388,7 @@ class TfMessageElement extends LitElement {
case 'md': case 'md':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'message')} @click=${() => (self.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -411,7 +396,7 @@ class TfMessageElement extends LitElement {
case 'decrypted': case 'decrypted':
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'raw')} @click=${() => (self.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
@ -420,136 +405,58 @@ class TfMessageElement extends LitElement {
if (this.message.decrypted) { if (this.message.decrypted) {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'decrypted')} @click=${() => (self.format = 'decrypted')}
> >
Decrypted Decrypted
</button>`; </button>`;
} else { } else {
raw_button = html`<button raw_button = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.format = 'raw')} @click=${() => (self.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
} }
break; break;
} }
return raw_button; function small_frame(inner) {
} let body;
return html`
render_header() { <div
let is_encrypted = this.message?.decrypted class="w3-card-4 ${class_background} w3-border-theme"
? html`<span class="w3-bar-item">🔓</span>` style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
: typeof this.message?.content == 'string'
? html`<span class="w3-bar-item">🔒</span>`
: undefined;
return html`
<header class="w3-bar">
<span class="w3-bar-item">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
</span>
${is_encrypted}
<span class="w3-bar-item w3-right">${this.render_raw_button()}</span>
<span class="w3-bar-item w3-right" style="text-wrap: nowrap"
><a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
> >
</header> <tf-user id=${self.message.author} .users=${self.users}></tf-user>
`; <span style="padding-right: 8px"
} ><a tfarget="_top" href=${'#' + encodeURIComponent(self.message.id)}
>%</a
render_frame(inner) { >
return html` ${new Date(self.message.timestamp).toLocaleString()}</span
<style> >
code { ${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
white-space: pre-wrap; ${self.render_votes()}
overflow-wrap: break-word; ${(self.message.child_messages || []).map(
} (x) => html`
div { <tf-message
overflow-wrap: anywhere; .message=${x}
} whoami=${self.whoami}
img { .users=${self.users}
max-width: 100%; .drafts=${self.drafts}
height: auto; .expanded=${self.expanded}
display: block; channel=${self.channel}
} channel_unread=${self.channel_unread}
</style> ></tf-message>
<div `
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top" )}
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%" </div>
> `;
${inner}
</div>
`;
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
${self.render_header()}
${self.format == 'raw'
? html`<div class="w3-container">${self.render_raw()}</div>`
: inner}
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
></tf-message>
`
)}
`);
}
render_actions() {
let content = this.get_content();
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<div class="w3-section w3-container">
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</div>
`;
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
} }
let class_background = this.class_background();
let self = this;
if (this.message?.type === 'contact_group') { if (this.message?.type === 'contact_group') {
return this.render_frame( return html` <div
html` ${this.message.messages.map( class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) => (x) =>
html`<tf-message html`<tf-message
.message=${x} .message=${x}
@ -560,38 +467,39 @@ class TfMessageElement extends LitElement {
channel=${this.channel} channel=${this.channel}
channel_unread=${this.channel_unread} channel_unread=${this.channel_unread}
></tf-message>` ></tf-message>`
)}` )}
); </div>`;
} else if (this.message.placeholder) { } else if (this.message.placeholder) {
return this.render_frame( return html` <div
html` <a target="_top" href=${'#' + encodeURIComponent(this.message.id)} class="w3-card-4 ${class_background} w3-border-theme"
>${this.message.id}</a style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
> >
(placeholder) <a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
<div>${this.render_votes()}</div> >${this.message.id}</a
${(this.message.child_messages || []).map( >
(x) => html` (placeholder)
<tf-message <div>${this.render_votes()}</div>
.message=${x} ${(this.message.child_messages || []).map(
whoami=${this.whoami} (x) => html`
.users=${this.users} <tf-message
.drafts=${this.drafts} .message=${x}
.expanded=${this.expanded} whoami=${this.whoami}
channel=${this.channel} .users=${this.users}
channel_unread=${this.channel_unread} .drafts=${this.drafts}
></tf-message> .expanded=${this.expanded}
` channel=${this.channel}
)}` channel_unread=${this.channel_unread}
); ></tf-message>
} else if (typeof content?.type === 'string') { `
)}
</div>`;
} else if (typeof (content?.type === 'string')) {
if (content.type == 'about') { if (content.type == 'about') {
let name; let name;
let image; let image;
let description; let description;
if (content.name !== undefined) { if (content.name !== undefined) {
name = html`<div> name = html`<div><b>Name:</b> ${content.name}</div>`;
<b>Name:</b> ${content.name}
</div>`;
} }
if (content.image !== undefined) { if (content.image !== undefined) {
image = html` image = html`
@ -600,30 +508,22 @@ class TfMessageElement extends LitElement {
} }
if (content.description !== undefined) { if (content.description !== undefined) {
description = html` description = html`
<div <div style="flex: 1 0 50%; overflow-wrap: anywhere">
style="flex: 1 0 50%; overflow-wrap: anywhere"
>
<div>${unsafeHTML(tfutils.markdown(content.description))}</div> <div>${unsafeHTML(tfutils.markdown(content.description))}</div>
</div> </div>
`; `;
} }
let update = let update =
content.about == this.message.author content.about == this.message.author
? html`<div style="font-weight: bold"> ? html`<div style="font-weight: bold">Updated profile.</div>`
Updated profile.
</div>`
: html`<div style="font-weight: bold"> : html`<div style="font-weight: bold">
Updated profile for Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>. <tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`; </div>`;
return this.render_small_frame(html` return small_frame(html` ${update} ${name} ${image} ${description} `);
<div class="w3-container">
<p>${update} ${name} ${image} ${description}</p>
</div>
`);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return html` return html`
<div class="w3-padding"> <div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
is is
${content.blocking === true ${content.blocking === true
@ -642,6 +542,24 @@ class TfMessageElement extends LitElement {
</div> </div>
`; `;
} else if (content.type == 'post') { } else if (content.type == 'post') {
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
let self = this; let self = this;
let body; let body;
switch (this.format) { switch (this.format) {
@ -658,7 +576,11 @@ class TfMessageElement extends LitElement {
body = unsafeHTML(tfutils.markdown(content.text)); body = unsafeHTML(tfutils.markdown(content.text));
break; break;
case 'decrypted': case 'decrypted':
body = this.render_json(content); body = html`<pre
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break; break;
} }
let content_warning = html` let content_warning = html`
@ -680,22 +602,108 @@ class TfMessageElement extends LitElement {
? html` ${content_warning} ${content_html} ` ? html` ${content_warning} ${content_html} `
: content_warning : content_warning
: content_html; : content_html;
return this.render_frame(html` let is_encrypted = this.message?.decrypted
${this.render_header()} ? html`<span style="align-self: center">🔓</span>`
<div class="w3-container">${payload}</div> : undefined;
${this.render_votes()} ${this.render_actions()} return html`
</div> <style>
`); code {
} else if (content.type === 'issue') { white-space: pre-wrap;
return this.render_frame(html` overflow-wrap: break-word;
${this.render_header()} ${content.text} ${this.render_votes()} }
<footer class="w3-container"> div {
<button class="w3-button w3-theme-d1" @click=${this.react}> overflow-wrap: anywhere;
React }
</button> img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="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=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${payload} ${this.render_votes()}
<p>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${!content.root && this.message.rowid < this.channel_unread
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.mark_unread}
>
Mark Unread
</button>
`
: undefined}
</p>
${this.render_children()} ${this.render_children()}
</footer> </div>
`); `;
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="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=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${content.text} ${this.render_votes()}
<p>
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') { } else if (content.type === 'blog') {
let self = this; let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) { tfrpc.rpc.get_blob(content.blog).then(function (data) {
@ -731,14 +739,70 @@ class TfMessageElement extends LitElement {
`; `;
break; break;
} }
return this.render_frame(html` let reply =
${this.render_header()} this.drafts[this.message?.id] !== undefined
<div>${body}</div> ? html`
${this.render_mentions()} ${this.render_votes()} <tf-compose
${this.render_actions()} whoami=${this.whoami}
`); .users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
<div>${body}</div>
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</div>
${this.render_votes()} ${this.render_children()}
</div>
`;
} else if (content.type === 'pub') { } else if (content.type === 'pub') {
return this.render_small_frame( return small_frame(
html` <style> html` <style>
span { span {
overflow-wrap: anywhere; overflow-wrap: anywhere;
@ -756,42 +820,35 @@ class TfMessageElement extends LitElement {
</span>` </span>`
); );
} else if (content.type === 'channel') { } else if (content.type === 'channel') {
return this.render_small_frame(html` return small_frame(html`
<div class="w3-container"> <div>
<p>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} ${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#' + encodeURIComponent('#' + content.channel)} <a href=${'#' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a >#${content.channel}</a
> >
</p>
</div> </div>
`); `);
} else if (typeof this.message.content == 'string') { } else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) { if (this.message?.decrypted) {
if (this.format == 'decrypted') { if (this.format == 'decrypted') {
return this.render_small_frame( return small_frame(
html`<span class="w3-container">🔓</span> ${this.render_json( html`<span>🔓</span>
this.message.decrypted <pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
)}`
); );
} else { } else {
return this.render_small_frame( return small_frame(
html`<span class="w3-container">🔓</span> html`<span>🔓</span>
<div class="w3-container">${this.message.decrypted.type}</div>` <div>${this.message.decrypted.type}</div>`
); );
} }
} else { } else {
return this.render_small_frame(); return small_frame(html`<span>🔒</span>`);
} }
} else { } else {
return this.render_small_frame( return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
html`<div class="w3-container"><b>type</b>: ${content.type}</div>`
);
} }
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
} else { } else {
return this.render_small_frame(this.render_raw()); return small_frame(this.render_raw());
} }
} }
} }

View File

@ -210,7 +210,7 @@ class TfNewsElement extends LitElement {
channel=${this.channel} channel=${this.channel}
channel_unread=${this.channel_unread} channel_unread=${this.channel_unread}
></tf-message> ></tf-message>
${x.rowid == unread_rowid ${x.rowid == unread_rowid && x != final_messages[0]
? html`<div style="display: flex; flex-direction: row"> ? html`<div style="display: flex; flex-direction: row">
<div <div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px" style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"

View File

@ -11,6 +11,7 @@ class TfProfileElement extends LitElement {
id: {type: String}, id: {type: String},
users: {type: Object}, users: {type: Object},
size: {type: Number}, size: {type: Number},
server_follows_me: {type: Boolean},
following: {type: Boolean}, following: {type: Boolean},
blocking: {type: Boolean}, blocking: {type: Boolean},
}; };
@ -26,6 +27,7 @@ class TfProfileElement extends LitElement {
this.id = null; this.id = null;
this.users = {}; this.users = {};
this.size = 0; this.size = 0;
this.server_follows_me = undefined;
} }
async load() { async load() {
@ -61,6 +63,26 @@ class TfProfileElement extends LitElement {
} }
} }
async initial_load() {
this.server_follows_me = undefined;
let server_id = await tfrpc.rpc.getServerIdentity();
let followed = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages
WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`,
[server_id, this.whoami]
);
let is_followed = false;
for (let row of followed) {
is_followed = row.following != 0;
}
this.server_follows_me = is_followed;
}
modify(change) { modify(change) {
tfrpc.rpc tfrpc.rpc
.appendMessage( .appendMessage(
@ -153,11 +175,31 @@ class TfProfileElement extends LitElement {
input.click(); input.click();
} }
async server_follow_me(follow) {
try {
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
} catch (e) {
console.log(e);
}
try {
await this.initial_load();
} catch (e) {
console.log(e);
}
}
copy_id() { copy_id() {
navigator.clipboard.writeText(this.id); navigator.clipboard.writeText(this.id);
} }
render() { render() {
if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load();
}
this.load(); this.load();
let self = this; let self = this;
let profile = this.users[this.id] || {}; let profile = this.users[this.id] || {};
@ -174,6 +216,22 @@ class TfProfileElement extends LitElement {
let block; let block;
if (this.id === this.whoami) { if (this.id === this.whoami) {
if (this.editing) { if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html` edit = html`
<button <button
id="save_profile" id="save_profile"
@ -185,6 +243,7 @@ class TfProfileElement extends LitElement {
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard Discard
</button> </button>
${server_follow}
`; `;
} else { } else {
edit = html`<button edit = html`<button
@ -217,18 +276,20 @@ class TfProfileElement extends LitElement {
let edit_profile = this.editing let edit_profile = this.editing
? html` ? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px"> <div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div> <div class="w3-container">
<label for="name">Name:</label> <div>
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))} placeholder="Choose a name"></input> <label for="name">Name:</label>
</div> <input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
<div><label for="description">Description:</label></div> </div>
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea> <div><label for="description">Description:</label></div>
<div> <textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
<label for="public_web_hosting">Public Web Hosting:</label> <div>
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input> <label for="public_web_hosting">Public Web Hosting:</label>
</div> <input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
<div> </div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button> <div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div> </div>
</div>` </div>`
: null; : null;
@ -236,36 +297,28 @@ class TfProfileElement extends LitElement {
typeof profile.image == 'string' ? profile.image : profile.image?.link; typeof profile.image == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image; image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description; let description = this.editing?.description ?? profile.description;
return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box"> return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
<header class="w3-container"> <p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p> <input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
</header> <button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
<div class="w3-container"> <div style="display: flex; flex-direction: row; gap: 1em">
<div class="w3-margin-bottom" style="display: flex; flex-direction: row"> ${edit_profile}
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input> <div style="flex: 1 0 50%">
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button> <div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
</div> <div>${unsafeHTML(tfutils.markdown(description))}</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
<div>
Following ${profile.following} identities.
Followed by ${profile.followed} identities.
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
</div> </div>
</div> </div>
<footer class="w3-container"> <div>
<p> Following ${profile.following} identities.
${edit} Followed by ${profile.followed} identities.
${follow} Blocking ${profile.blocking} identities.
${block} Blocked by ${profile.blocked} identities.
</p> </div>
</footer> <p>
${edit}
${follow}
${block}
</p>
</div>`; </div>`;
} }
} }

View File

@ -1,4 +1,4 @@
import {css, unsafeCSS} from './lit-all.min.js'; import {css} from './lit-all.min.js';
const tf = css` const tf = css`
img { img {
@ -285,165 +285,30 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
`; `;
function rgb_to_hsl(r, g, b) { // prettier-ignore
let min, const w3_2016_snorkel_blue = css`
max, .w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
i, .w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
l, .w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
s, .w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
maxcolor, .w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
h, .w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
rgb = []; .w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
rgb[0] = r / 255; .w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
rgb[1] = g / 255; .w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
rgb[2] = b / 255; .w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
min = rgb[0];
max = rgb[0];
maxcolor = 0;
for (i = 0; i < rgb.length - 1; i++) {
if (rgb[i + 1] <= min) {
min = rgb[i + 1];
}
if (rgb[i + 1] >= max) {
max = rgb[i + 1];
maxcolor = i + 1;
}
}
if (maxcolor == 0) {
h = (rgb[1] - rgb[2]) / (max - min);
}
if (maxcolor == 1) {
h = 2 + (rgb[2] - rgb[0]) / (max - min);
}
if (maxcolor == 2) {
h = 4 + (rgb[0] - rgb[1]) / (max - min);
}
if (isNaN(h)) {
h = 0;
}
h = h * 60;
if (h < 0) {
h = h + 360;
}
l = (min + max) / 2;
if (min == max) {
s = 0;
} else {
if (l < 0.5) {
s = (max - min) / (max + min);
} else {
s = (max - min) / (2 - max - min);
}
}
s = s;
return [h, s, l];
}
function hex_to_rgb(hex) { .w3-theme-light {color:#000 !important; background-color:#e9f5ff !important}
if (hex.charAt(0) == '#') { .w3-theme-dark {color:#fff !important; background-color:#012641 !important}
hex = hex.substring(1); .w3-theme-action {color:#fff !important; background-color:#012641 !important}
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
}
function hsl_to_rgb(hue, sat, light) { .w3-theme {color:#fff !important; background-color:#034f84 !important}
let t2; .w3-text-theme {color:#034f84 !important}
hue /= 60; .w3-border-theme {border-color:#034f84 !important}
if (light <= 0.5) {
t2 = light * (sat + 1);
} else {
t2 = light + sat - light * sat;
}
let t1 = light * 2 - t2;
return [
hue_to_rgb(t1, t2, hue + 2) * 255,
hue_to_rgb(t1, t2, hue) * 255,
hue_to_rgb(t1, t2, hue - 2) * 255,
];
}
function hue_to_rgb(t1, t2, hue) {
if (hue < 0) {
hue += 6;
}
if (hue >= 6) {
hue -= 6;
}
if (hue < 1) {
return (t2 - t1) * hue + t1;
} else if (hue < 3) {
return t2;
} else if (hue < 4) {
return (t2 - t1) * (4 - hue) + t1;
} else {
return t1;
}
}
function rgb_to_hex(rgb) { .w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important}
const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0'); .w3-hover-text-theme:hover {color:#034f84 !important}
return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`; .w3-hover-border-theme:hover {border-color:#034f84 !important}
} `;
function is_dark(hex, value) { export let styles = [tf, w3, w3_2016_snorkel_blue];
let [r, g, b] = hex_to_rgb(hex);
return (r * 299 + g * 587 + b * 114) / 1000 < value;
}
function generated() {
let now = new Date();
let k_color = rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
]);
//let k_color = '#034f84';
//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);
let [r, g, b] = hex_to_rgb(k_color);
let [h, s, l] = rgb_to_hsl(r, g, b);
let theme1 = {
l5: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4.7)),
l4: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4)),
l3: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 3)),
l2: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 2)),
l1: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 1)),
d0: rgb_to_hex(hsl_to_rgb(h, s, l)),
d1: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 0.5)),
d2: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1)),
d3: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1.5)),
d4: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2)),
d5: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2.5)),
};
for (let [k, v] of Object.entries(theme1)) {
theme1['t' + k] = is_dark(v, 165) ? '#fff' : '#000';
}
let result = `
.w3-theme-l5 {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
.w3-theme-l4 {color: ${theme1.tl4} !important; background-color: ${theme1.l4} !important}
.w3-theme-l3 {color: ${theme1.tl3} !important; background-color: ${theme1.l3} !important}
.w3-theme-l2 {color: ${theme1.tl2} !important; background-color: ${theme1.l2} !important}
.w3-theme-l1 {color: ${theme1.tl1} !important; background-color: ${theme1.l1} !important}
.w3-theme-d1 {color: ${theme1.td1} !important; background-color: ${theme1.d1} !important}
.w3-theme-d2 {color: ${theme1.td2} !important; background-color: ${theme1.d2} !important}
.w3-theme-d3 {color: ${theme1.td3} !important; background-color: ${theme1.d3} !important}
.w3-theme-d4 {color: ${theme1.td4} !important; background-color: ${theme1.d4} !important}
.w3-theme-d5 {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme-light {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
.w3-theme-dark {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme-action {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
.w3-text-theme {color: ${theme1.d0} !important}
.w3-border-theme {border-color: ${theme1.d0} !important}
.w3-hover-theme:hover {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
.w3-hover-text-theme:hover {color: ${theme1.d0} !important}
.w3-hover-border-theme:hover {border-color: ${theme1.d0} !important}
`;
return unsafeCSS(result);
}
export let styles = [tf, w3, generated()];

View File

@ -175,9 +175,6 @@ class TfTabConnectionsElement extends LitElement {
.map((x) => html`<li>${this.render_connection(x)}</li>`)} .map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)} ${this.render_room_peers(connection.id)}
</ul> </ul>
<div ?hidden=${!connection.destroy_reason} class="w3-panel w3-red">
<p>${connection.destroy_reason}</p>
</div>
`; `;
} }

View File

@ -51,21 +51,15 @@ class TfTabNewsFeedElement extends LitElement {
if (this.hash == '#@') { if (this.hash == '#@') {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?1)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.author != ?1 AND
messages.timestamp >= ?3 AND
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20)
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions FROM messages_fts(?1)
JOIN messages_refs ON mentions.id = messages_refs.ref JOIN messages ON messages.rowid = messages_fts.rowid
JOIN messages ON messages_refs.message = messages.id JOIN json_each(?2) AS following ON messages.author = following.value
UNION WHERE
SELECT * FROM mentions messages.author != ?1 AND
messages.timestamp >= ?3 AND
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20
`, `,
[ [
'"' + this.whoami.replace('"', '""') + '"', '"' + this.whoami.replace('"', '""') + '"',
@ -294,12 +288,7 @@ class TfTabNewsFeedElement extends LitElement {
this.loading--; this.loading--;
} }
this.messages = Object.values( this.messages = Object.values(
Object.fromEntries( Object.fromEntries([...this.messages, ...messages].map((x) => [x.id, x]))
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
); );
console.log('done loading latest messages.'); console.log('done loading latest messages.');
} }

View File

@ -8,6 +8,7 @@ class TfTabNewsElement extends LitElement {
whoami: {type: String}, whoami: {type: String},
users: {type: Object}, users: {type: Object},
hash: {type: String}, hash: {type: String},
unread: {type: Array},
following: {type: Array}, following: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
@ -15,7 +16,6 @@ class TfTabNewsElement extends LitElement {
channels: {type: Array}, channels: {type: Array},
channels_unread: {type: Object}, channels_unread: {type: Object},
channels_latest: {type: Object}, channels_latest: {type: Object},
connections: {type: Array},
}; };
} }
@ -27,6 +27,7 @@ class TfTabNewsElement extends LitElement {
this.whoami = null; this.whoami = null;
this.users = {}; this.users = {};
this.hash = '#'; this.hash = '#';
this.unread = [];
this.following = []; this.following = [];
this.cache = {}; this.cache = {};
this.drafts = {}; this.drafts = {};
@ -34,7 +35,6 @@ class TfTabNewsElement extends LitElement {
this.channels_unread = {}; this.channels_unread = {};
this.channels_latest = {}; this.channels_latest = {};
this.channels = []; this.channels = [];
this.connections = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) { tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}'); self.drafts = JSON.parse(d || '{}');
}); });
@ -50,13 +50,36 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this)); document.body.removeEventListener('keypress', this.on_keypress.bind(this));
} }
load_latest() { show_more() {
let unread = this.unread;
let news = this.shadowRoot?.getElementById('news'); let news = this.shadowRoot?.getElementById('news');
if (news) { if (news) {
news.load_latest(); news.load_latest();
this.dispatchEvent(new CustomEvent('refresh'));
} }
} }
new_messages_text() {
if (!this.unread?.length) {
return 'No new messages.';
}
let counts = {};
for (let message of this.unread) {
let type = 'private';
try {
type = JSON.parse(message.content).type || type;
} catch {}
counts[type] = (counts[type] || 0) + 1;
}
return (
'↻ Show New: ' +
Object.keys(counts)
.sort()
.map((x) => counts[x].toString() + ' ' + x + 's')
.join(', ')
);
}
draft(event) { draft(event) {
let id = event.detail.id || ''; let id = event.detail.id || '';
let previous = this.drafts[id]; let previous = this.drafts[id];
@ -89,11 +112,10 @@ class TfTabNewsElement extends LitElement {
unread_status(channel) { unread_status(channel) {
if ( if (
this.channels_latest[channel] && this.channels_latest[channel] &&
this.channels_latest[channel] > 0 &&
(this.channels_unread[channel] === undefined || (this.channels_unread[channel] === undefined ||
this.channels_unread[channel] <= this.channels_latest[channel]) this.channels_unread[channel] <= this.channels_latest[channel])
) { ) {
return '✉️ '; return '🔵';
} }
} }
@ -128,11 +150,33 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
} }
render_sidebar() { render() {
let profile =
this.hash.startsWith('#@') && this.hash != '#@'
? html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
let edit_profile;
if (
!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami
) {
edit_profile = html` <div
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
>
Follow your identity link above to edit your profile and set your
name.
</div>`;
}
return html` return html`
<div <div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left" class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0" style="width: 2in; left: 0; z-index: 5"
id="sidebar" id="sidebar"
> >
<div <div
@ -158,144 +202,89 @@ class TfTabNewsElement extends LitElement {
href="#" href="#"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style=${this.hash == '#' ? 'font-weight: bold' : undefined} style=${this.hash == '#' ? 'font-weight: bold' : undefined}
>${this.unread_status('')}general</a >general ${this.unread_status('')}</a
> >
<a <a
href="#@" href="#@"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style=${this.hash == '#@' ? 'font-weight: bold' : undefined} style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>${this.unread_status('@')}@mentions</a >@mentions ${this.unread_status('@')}</a
> >
<a <a
href="#🔐" href="#🔐"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined} style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>${this.unread_status('🔐')}🔐private</a >🔐private ${this.unread_status('🔐')}</a
> >
${Object.keys(this.drafts)
.sort()
.map(
(x) => html`
<a
href=${'#' + encodeURIComponent(x)}
class="w3-bar-item w3-button"
style="text-wrap: nowrap; text-overflow: ellipsis"
>📝 ${this.drafts[x]?.text ?? x}</a
>
`
)}
${this.channels.map( ${this.channels.map(
(x) => html` (x) => html`
<a <a
href=${'#' + encodeURIComponent('#' + x)} href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined} style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
>${this.unread_status(x)}#${x}</a >#${x} ${this.unread_status(x)}</a
> >
` `
)} )}
<div class="w3-bar-item w3-theme-d2">Connections</div>
${this.connections.map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x.id}
.users=${this.users}
></tf-user>
`
)}
</div> </div>
<div <div
class="w3-overlay" class="w3-overlay"
id="sidebar_overlay" id="sidebar_overlay"
@click=${this.hide_sidebar} @click=${this.hide_sidebar}
></div> ></div>
`; <div style="margin-left: 2in; padding: 8px" id="main" class="w3-main">
} <div
id="show_sidebar"
render() { class="w3-left w3-button w3-hide-large"
let profile = @click=${this.show_sidebar}
this.hash.startsWith('#@') && this.hash != '#@' >
? html`<tf-profile &#9776;
class="tf-profile"
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
let edit_profile;
if (
!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami
) {
edit_profile = html` <div
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
>
Follow your identity link above to edit your profile and set your
name.
</div>`;
}
return html`
${this.render_sidebar()}
<div
style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
id="main"
class="w3-main"
>
<div style="padding: 8px">
<p>
${this.hash.startsWith('##')
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.channel_toggle_subscribed}
>
${this.channels.indexOf(this.hash.substring(2)) != -1
? 'Unsubscribe from #'
: 'Subscribe to #'}${this.hash.substring(2)}
</button>
`
: undefined}
</p>
<div>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
&#9776;
</div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose>
</div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed>
</div> </div>
<p>
<button class="w3-button w3-theme-d1" @click=${this.show_more}>
${this.new_messages_text()}
</button>
${this.hash.startsWith('##')
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.channel_toggle_subscribed}
>
${this.channels.indexOf(this.hash.substring(2)) != -1
? 'Unsubscribe from #'
: 'Subscribe to #'}${this.hash.substring(2)}
</button>
`
: undefined}
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose>
</div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed>
</div> </div>
`; `;
} }

View File

@ -19,9 +19,9 @@ class TfTagElement extends LitElement {
let number = this.count ? html` (${this.count})` : undefined; let number = this.count ? html` (${this.count})` : undefined;
return html`<a return html`<a
href=${'#' + encodeURIComponent(this.tag)} href=${'#' + encodeURIComponent(this.tag)}
class="w3-tag w3-theme-d1 w3-round-4 w3-button" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
>${this.tag}${number}</a >${this.tag}${number}</a
> `; >`;
} }
} }

View File

@ -25,9 +25,10 @@ class TfUserElement extends LitElement {
>?</span >?</span
>`; >`;
let name = this.users?.[this.id]?.name; let name = this.users?.[this.id]?.name;
name = html`<a target="_top" href=${'#' + this.id} name =
>${name !== undefined ? name : this.id}</a 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]) { if (this.users[this.id]) {
let image_link = this.users[this.id].image; let image_link = this.users[this.id].image;
@ -41,9 +42,7 @@ class TfUserElement extends LitElement {
/>`; />`;
} }
} }
return html` <div return html` <div style="display: inline-block; font-weight: bold">
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
>
${image} ${name} ${image} ${name}
</div>`; </div>`;
} }

View File

@ -577,6 +577,19 @@ async function getProcessBlob(blobId, key, options) {
); );
} }
}; };
imports.ssb.setServerFollowingMe = function (id, following) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.setServerFollowingMe(
process.credentials.session.name,
id,
following
);
}
};
imports.ssb.swapWithServerIdentity = function (id) { imports.ssb.swapWithServerIdentity = function (id) {
if ( if (
process.credentials && process.credentials &&

View File

@ -21,14 +21,14 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.0.26"; version = "0.0.25";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-XJ7M++risfsRn9GkS1zjTQpqqV5S09uyimeVzU9hGGg="; hash = "sha256-Rfk+CUhi+Ss0z70CCgmtVM/w4nCL1GX/MsD4sPYIa5s=";
fetchSubmodules = true; fetchSubmodules = true;
}; };

File diff suppressed because one or more lines are too long

View File

@ -98,9 +98,9 @@
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.10.8", "version": "6.10.7",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.7.tgz",
"integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==", "integrity": "sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@ -278,9 +278,9 @@
} }
}, },
"node_modules/@lezer/json": { "node_modules/@lezer/json": {
"version": "1.0.3", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",

View File

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

View File

@ -738,6 +738,25 @@ static void _httpd_endpoint_mem(tf_http_request_t* request)
tf_free(response); tf_free(response);
} }
static void _httpd_endpoint_disconnections(tf_http_request_t* request)
{
if (_httpd_redirect(request))
{
return;
}
tf_task_t* task = request->user_data;
char* response = tf_task_get_disconnections(task);
const char* headers[] = {
"Content-Type",
"application/json; charset=utf-8",
"Access-Control-Allow-Origin",
"*",
};
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, response, response ? strlen(response) : 0);
tf_free(response);
}
static void _httpd_endpoint_hitches(tf_http_request_t* request) static void _httpd_endpoint_hitches(tf_http_request_t* request)
{ {
if (_httpd_redirect(request)) if (_httpd_redirect(request))
@ -2080,26 +2099,19 @@ static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
if (form_register && strcmp(form_register, "1") == 0) if (form_register && strcmp(form_register, "1") == 0)
{ {
bool registered = false; bool registered = false;
if (!_is_name_valid(account_name)) if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{ {
login_error = "Invalid username. Usernames must contain only letters from the English alphabet and digits and must start with a letter."; sqlite3* db = tf_ssb_acquire_db_writer(ssb);
} registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password);
else tf_ssb_release_db_writer(ssb, db);
{ if (registered)
if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{ {
sqlite3* db = tf_ssb_acquire_db_writer(ssb); tf_free((void*)send_session);
registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password); send_session = _make_session_jwt(context, ssb, account_name);
tf_ssb_release_db_writer(ssb, db); may_become_first_admin = true;
if (registered)
{
tf_free((void*)send_session);
send_session = _make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
} }
} }
if (!registered && !login_error) if (!registered)
{ {
login_error = "Error registering account."; login_error = "Error registering account.";
} }
@ -2305,6 +2317,7 @@ void tf_httpd_register(JSContext* context)
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL); tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task); tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);
tf_http_add_handler(http, "/disconnections", _httpd_endpoint_disconnections, NULL, task);
tf_http_add_handler(http, "/hitches", _httpd_endpoint_hitches, NULL, task); tf_http_add_handler(http, "/hitches", _httpd_endpoint_hitches, NULL, task);
tf_http_add_handler(http, "/mem", _httpd_endpoint_mem, NULL, task); tf_http_add_handler(http, "/mem", _httpd_endpoint_mem, NULL, task);
tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, NULL, task); tf_http_add_handler(http, "/trace", _httpd_endpoint_trace, NULL, task);

View File

@ -15,7 +15,6 @@
#include "unzip.h" #include "unzip.h"
#include <getopt.h> #include <getopt.h>
#include <inttypes.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -26,7 +25,6 @@
#if defined(__linux__) #if defined(__linux__)
#include <sys/prctl.h> #include <sys/prctl.h>
#include <sys/stat.h>
#endif #endif
#if defined(__APPLE__) #if defined(__APPLE__)
@ -43,102 +41,7 @@
struct backtrace_state* g_backtrace_state; struct backtrace_state* g_backtrace_state;
#if !TARGET_OS_IPHONE const char* k_db_path_default = "db.sqlite";
static const char* _get_db_path()
{
const char* k_db_path_default = "db.sqlite";
#if defined(__linux__)
if (stat(k_db_path_default, &(struct stat) { 0 }) == 0)
{
return tf_strdup(k_db_path_default);
}
else
{
char buffer[32];
char* data_home = NULL;
size_t size = sizeof(buffer);
int r = uv_os_getenv("XDG_DATA_HOME", buffer, &size);
if (r == 0 || r == UV_ENOBUFS)
{
size++;
data_home = alloca(size);
if (uv_os_getenv("XDG_DATA_HOME", data_home, &size) != 0)
{
data_home = NULL;
}
}
if (!data_home)
{
size = sizeof(buffer);
r = uv_os_getenv("HOME", buffer, &size);
if (r == 0 || r == UV_ENOBUFS)
{
size++;
char* home = alloca(size);
r = uv_os_getenv("HOME", home, &size);
if (r == 0)
{
size = snprintf(NULL, 0, "%s/.local/share", home) + 1;
data_home = alloca(size);
snprintf(data_home, size, "%s/.local/share", home);
}
}
}
if (data_home)
{
size = snprintf(NULL, 0, "%s/tildefriends/db.sqlite", data_home) + 1;
char* path = alloca(size);
snprintf(path, size, "%s/tildefriends/db.sqlite", data_home);
return tf_strdup(path);
}
}
#endif
return tf_strdup(k_db_path_default);
}
static void _create_directories_for_file(const char* path, int mode)
{
if (stat(path, &(struct stat) { 0 }) == 0)
{
/* It already exists. OK. */
return;
}
size_t length = strlen(path) + 1;
char* copy = alloca(length);
memcpy(copy, path, length);
#if defined(_WIN32)
for (char* c = copy; *c; c++)
{
if (*c == '\\')
{
*c = '/';
}
}
#endif
char* slash = copy;
while (slash)
{
slash = strchr(slash + 1, '/');
if (slash)
{
*slash = '\0';
#if defined(_WIN32)
if (mkdir(copy) == 0)
#else
if (mkdir(copy, mode) == 0)
#endif
{
tf_printf("Created directory %s.\n", copy);
}
*slash = '/';
}
}
}
#endif
#if !TARGET_OS_IPHONE && !defined(__ANDROID__) #if !TARGET_OS_IPHONE && !defined(__ANDROID__)
static int _tf_command_export(const char* file, int argc, char* argv[]); static int _tf_command_export(const char* file, int argc, char* argv[]);
@ -146,11 +49,7 @@ static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_publish(const char* file, int argc, char* argv[]); static int _tf_command_publish(const char* file, int argc, char* argv[]);
static int _tf_command_run(const char* file, int argc, char* argv[]); static int _tf_command_run(const char* file, int argc, char* argv[]);
static int _tf_command_sandbox(const char* file, int argc, char* argv[]); static int _tf_command_sandbox(const char* file, int argc, char* argv[]);
static int _tf_command_has_blob(const char* file, int argc, char* argv[]);
static int _tf_command_store_blob(const char* file, int argc, char* argv[]); static int _tf_command_store_blob(const char* file, int argc, char* argv[]);
static int _tf_command_get_sequence(const char* file, int argc, char* argv[]);
static int _tf_command_get_identity(const char* file, int argc, char* argv[]);
static int _tf_command_get_profile(const char* file, int argc, char* argv[]);
static int _tf_command_test(const char* file, int argc, char* argv[]); static int _tf_command_test(const char* file, int argc, char* argv[]);
static int _tf_command_verify(const char* file, int argc, char* argv[]); static int _tf_command_verify(const char* file, int argc, char* argv[]);
static int _tf_command_usage(const char* file); static int _tf_command_usage(const char* file);
@ -168,10 +67,6 @@ const command_t k_commands[] = {
{ "import", _tf_command_import, "Import apps to SSB." }, { "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." }, { "export", _tf_command_export, "Export apps from SSB." },
{ "publish", _tf_command_publish, "Append a message to a feed." }, { "publish", _tf_command_publish, "Append a message to a feed." },
{ "get_sequence", _tf_command_get_sequence, "Get the last sequence number for a feed." },
{ "get_identity", _tf_command_get_identity, "Get the server account identity." },
{ "get_profile", _tf_command_get_profile, "Get profile information for the given identity." },
{ "has_blob", _tf_command_has_blob, "Check whether a blob is in the blob store." },
{ "store_blob", _tf_command_store_blob, "Write a file to the blob store." }, { "store_blob", _tf_command_store_blob, "Write a file to the blob store." },
{ "verify", _tf_command_verify, "Verify a feed." }, { "verify", _tf_command_verify, "Verify a feed." },
{ "test", _tf_command_test, "Test SSB." }, { "test", _tf_command_test, "Test SSB." },
@ -236,8 +131,7 @@ static int _tf_command_test(const char* file, int argc, char* argv[])
static int _tf_command_import(const char* file, int argc, char* argv[]) static int _tf_command_import(const char* file, int argc, char* argv[])
{ {
const char* user = "import"; const char* user = "import";
const char* default_db_path = _get_db_path(); const char* db_path = k_db_path_default;
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -275,13 +169,11 @@ static int _tf_command_import(const char* file, int argc, char* argv[])
tf_printf("\n%s import [options] [paths...]\n\n", file); tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -u, --user user User into whose account apps will be imported (default: \"import\").\n"); tf_printf(" -u, --user user User into whose account apps will be imported (default: \"import\").\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
_create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
if (optind < argc) if (optind < argc)
{ {
@ -297,15 +189,13 @@ static int _tf_command_import(const char* file, int argc, char* argv[])
tf_ssb_import(ssb, user, "apps"); tf_ssb_import(ssb, user, "apps");
} }
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
static int _tf_command_export(const char* file, int argc, char* argv[]) static int _tf_command_export(const char* file, int argc, char* argv[])
{ {
const char* user = "core"; const char* user = "core";
const char* default_db_path = _get_db_path(); const char* db_path = k_db_path_default;
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -343,11 +233,10 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
tf_printf("\n%s export [options] [paths...]\n\n", file); tf_printf("\n%s export [options] [paths...]\n\n", file);
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -u, --user user User from whose account apps will be exported (default: \"core\").\n"); tf_printf(" -u, --user user User from whose account apps will be exported (default: \"core\").\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_printf("\n"); tf_printf("\n");
tf_printf("paths Paths of apps to export (example: /~core/ssb /~user/app).\n"); tf_printf("paths Paths of apps to export (example: /~core/ssb /~user/app).\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -382,7 +271,6 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
} }
} }
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
@ -410,8 +298,7 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
{ {
const char* user = NULL; const char* user = NULL;
const char* identity = NULL; const char* identity = NULL;
const char* default_db_path = _get_db_path(); const char* db_path = k_db_path_default;
const char* db_path = default_db_path;
const char* content = NULL; const char* content = NULL;
bool show_usage = false; bool show_usage = false;
@ -459,16 +346,14 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -u, --user user User owning identity with which to publish.\n"); tf_printf(" -u, --user user User owning identity with which to publish.\n");
tf_printf(" -i, --id identity Identity with which to publish message.\n"); tf_printf(" -i, --id identity Identity with which to publish message.\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -c, --content json JSON content of message to publish.\n"); tf_printf(" -c, --content json JSON content of message to publish.\n");
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
int result = EXIT_FAILURE; int result = EXIT_FAILURE;
tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user); tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user);
_create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
uint8_t private_key[512] = { 0 }; uint8_t private_key[512] = { 0 };
if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key))) if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
@ -501,14 +386,12 @@ static int _tf_command_publish(const char* file, int argc, char* argv[])
tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user); tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user);
} }
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result; return result;
} }
static int _tf_command_store_blob(const char* file, int argc, char* argv[]) static int _tf_command_store_blob(const char* file, int argc, char* argv[])
{ {
const char* default_db_path = _get_db_path(); const char* db_path = k_db_path_default;
const char* db_path = default_db_path;
const char* file_path = NULL; const char* file_path = NULL;
bool show_usage = false; bool show_usage = false;
@ -546,10 +429,9 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
{ {
tf_printf("\n%s store_blob [options]\n\n", file); tf_printf("\n%s store_blob [options]\n\n", file);
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -f, --file file_path Path to file to add to the blob store.\n"); tf_printf(" -f, --file file_path Path to file to add to the blob store.\n");
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -559,7 +441,6 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
if (!blob_file) if (!blob_file)
{ {
tf_printf("Failed to open %s: %s.\n", file_path, strerror(errno)); tf_printf("Failed to open %s: %s.\n", file_path, strerror(errno));
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -583,13 +464,11 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
tf_printf("Failed to read %s: %s.\n", file_path, strerror(errno)); tf_printf("Failed to read %s: %s.\n", file_path, strerror(errno));
fclose(blob_file); fclose(blob_file);
tf_free(data); tf_free(data);
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
fclose(blob_file); fclose(blob_file);
char id[256]; char id[256];
_create_directories_for_file(db_path, 0700);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL)) if (tf_ssb_db_blob_store(ssb, (const uint8_t*)data, size, id, sizeof(id), NULL))
{ {
@ -597,240 +476,13 @@ static int _tf_command_store_blob(const char* file, int argc, char* argv[])
} }
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
tf_free(data); tf_free(data);
tf_free((void*)default_db_path);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
static int _tf_command_has_blob(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* blob_id = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "blob_id", required_argument, NULL, 'b' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:b:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'b':
blob_id = optarg;
break;
}
}
if (show_usage || !blob_id)
{
tf_printf("\n%s has_blob [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -b, --blob_id blob_id ID of blob to query.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
sqlite3* db = NULL;
sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
tf_ssb_db_init_reader(db);
bool has = tf_ssb_db_blob_has(db, blob_id);
sqlite3_close(db);
tf_free((void*)default_db_path);
tf_printf("%s\n", has ? "true" : "false");
return has ? EXIT_SUCCESS : EXIT_FAILURE;
}
static int _tf_command_get_sequence(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "id", required_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
}
}
if (show_usage || !identity)
{
tf_printf("\n%s get_sequence [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
int64_t sequence = -1;
int result = tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, NULL, 0) ? EXIT_SUCCESS : EXIT_FAILURE;
tf_printf("%" PRId64 "\n", sequence);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_get_identity(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
}
}
if (show_usage)
{
tf_printf("\n%s get_identity [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
char id[k_id_base64_len] = { 0 };
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
int result = tf_ssb_whoami(ssb, id, sizeof(id)) ? EXIT_SUCCESS : EXIT_FAILURE;
tf_printf("%s\n", id);
tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return result;
}
static int _tf_command_get_profile(const char* file, int argc, char* argv[])
{
const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* identity = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "db-path", required_argument, NULL, 'd' },
{ "id", required_argument, NULL, 'i' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "d:i:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'd':
db_path = optarg;
break;
case 'i':
identity = optarg;
break;
}
}
if (show_usage || !identity)
{
tf_printf("\n%s get_profile [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
tf_printf(" -i, --identity identity Account from which to get latest sequence number.\n");
tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE;
}
sqlite3* db = NULL;
sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
tf_ssb_db_init_reader(db);
const char* profile = tf_ssb_db_get_profile(db, identity);
tf_printf("%s\n", profile);
sqlite3_close(db);
tf_free((void*)profile);
tf_free((void*)default_db_path);
return profile != NULL;
}
static int _tf_command_verify(const char* file, int argc, char* argv[]) static int _tf_command_verify(const char* file, int argc, char* argv[])
{ {
const char* identity = NULL; const char* identity = NULL;
const char* default_db_path = _get_db_path(); const char* db_path = k_db_path_default;
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -868,9 +520,8 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
tf_printf("\n%s import [options] [paths...]\n\n", file); tf_printf("\n%s import [options] [paths...]\n\n", file);
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -i, --identity identity Identity to verify.\n"); tf_printf(" -i, --identity identity Identity to verify.\n");
tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -878,7 +529,6 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL); tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
bool verified = tf_ssb_db_verify(ssb, identity); bool verified = tf_ssb_db_verify(ssb, identity);
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
tf_free((void*)default_db_path);
return verified ? EXIT_SUCCESS : EXIT_FAILURE; return verified ? EXIT_SUCCESS : EXIT_FAILURE;
} }
#endif #endif
@ -1023,14 +673,13 @@ static void _shed_privileges()
static int _tf_command_run(const char* file, int argc, char* argv[]) static int _tf_command_run(const char* file, int argc, char* argv[])
{ {
const char* default_db_path = _get_db_path();
tf_run_args_t args = { tf_run_args_t args = {
.count = 1, .count = 1,
.script = "core/core.js", .script = "core/core.js",
.http_port = 12345, .http_port = 12345,
.https_port = 12346, .https_port = 12346,
.ssb_port = 8008, .ssb_port = 8008,
.db_path = default_db_path, .db_path = k_db_path_default,
}; };
bool show_usage = false; bool show_usage = false;
@ -1115,7 +764,7 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
tf_printf(" -b, --ssb-port port Port on which to run SSB (default: 8008, 0 disables).\n"); tf_printf(" -b, --ssb-port port Port on which to run SSB (default: 8008, 0 disables).\n");
tf_printf(" -p, --http-port port Port on which to run Tilde Friends web server (default: 12345).\n"); tf_printf(" -p, --http-port port Port on which to run Tilde Friends web server (default: 12345).\n");
tf_printf(" -q, --https-port port Port on which to run secure Tilde Friends web server (default: 12346).\n"); tf_printf(" -q, --https-port port Port on which to run secure Tilde Friends web server (default: 12346).\n");
tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", default_db_path); tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", k_db_path_default);
tf_printf(" -k, --ssb-network-key key SSB network key to use.\n"); tf_printf(" -k, --ssb-network-key key SSB network key to use.\n");
tf_printf(" -n, --count count Number of instances to run.\n"); tf_printf(" -n, --count count Number of instances to run.\n");
tf_printf(" -a, --args args Arguments of the format key=value,foo=bar,verbose=true.\n"); tf_printf(" -a, --args args Arguments of the format key=value,foo=bar,verbose=true.\n");
@ -1123,7 +772,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
tf_printf(" -z, --zip path Zip archive from which to load files.\n"); tf_printf(" -z, --zip path Zip archive from which to load files.\n");
tf_printf(" -v, --verbose Log raw messages.\n"); tf_printf(" -v, --verbose Log raw messages.\n");
tf_printf(" -h, --help Show this usage information.\n"); tf_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -1132,7 +780,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
setpgid(0, 0); setpgid(0, 0);
#endif #endif
_create_directories_for_file(args.db_path, 0700);
if (args.count == 1) if (args.count == 1)
{ {
result = _tf_run_task(&args, 0); result = _tf_run_task(&args, 0);
@ -1160,7 +807,6 @@ static int _tf_command_run(const char* file, int argc, char* argv[])
tf_free(data); tf_free(data);
tf_free(threads); tf_free(threads);
} }
tf_free((void*)default_db_path);
return result; return result;
} }
@ -1242,21 +888,6 @@ static void _error_handler(int sig)
_exit(1); _exit(1);
} }
#if defined(_WIN32)
static LONG WINAPI _win32_exception_handler(EXCEPTION_POINTERS* info)
{
if (info->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION || info->ExceptionRecord->ExceptionCode == STATUS_ILLEGAL_INSTRUCTION ||
info->ExceptionRecord->ExceptionCode == STATUS_STACK_OVERFLOW || info->ExceptionRecord->ExceptionCode == STATUS_HEAP_CORRUPTION)
{
const char* stack = tf_util_backtrace_string();
tf_printf("ERROR:\n%s\n", stack);
tf_free((void*)stack);
_exit(1);
}
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
static void _startup(int argc, char* argv[]) static void _startup(int argc, char* argv[])
{ {
char buffer[8] = { 0 }; char buffer[8] = { 0 };
@ -1290,24 +921,20 @@ static void _startup(int argc, char* argv[])
#endif #endif
bool use_error_handler = false; bool use_error_handler = false;
#if defined(__ANDROID__) || defined(_WIN32) #if defined(__ANDROID__)
use_error_handler = true; use_error_handler = true;
#endif #endif
if (use_error_handler) if (use_error_handler)
{ {
if ( if (
#if !defined(_WIN32) #if !defined(_WIN32)
signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR ||
#endif #endif
signal(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSEGV, _error_handler) == SIG_ERR) signal(SIGSEGV, _error_handler) == SIG_ERR)
{ {
perror("signal"); perror("signal");
} }
} }
#if defined(_WIN32)
AddVectoredExceptionHandler(0, _win32_exception_handler);
#endif
} }
#if defined(__ANDROID__) #if defined(__ANDROID__)
@ -1461,7 +1088,7 @@ void tf_run_thread_start(const char* zip_path)
.http_port = 12345, .http_port = 12345,
.https_port = 12346, .https_port = 12346,
.ssb_port = 8008, .ssb_port = 8008,
.db_path = "db.sqlite", .db_path = k_db_path_default,
.one_proc = true, .one_proc = true,
.zip = zip_path, .zip = zip_path,
}; };

662
src/ssb.c

File diff suppressed because it is too large Load Diff

View File

@ -103,7 +103,7 @@ static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, v
uint8_t key_bin[k_id_bin_len]; uint8_t key_bin[k_id_bin_len];
if (tf_ssb_id_str_to_bin(key_bin, next->key)) if (tf_ssb_id_str_to_bin(key_bin, next->key))
{ {
tf_ssb_connect(ssb, next->host, next->port, key_bin, k_tf_ssb_connect_flag_do_not_store, NULL, NULL); tf_ssb_connect(ssb, next->host, next->port, key_bin, 0, NULL, NULL);
} }
} }
tf_free(next); tf_free(next);
@ -287,7 +287,7 @@ static void _tf_ssb_connections_sync_broadcast_visit(
} }
else else
{ {
tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot | k_tf_ssb_connect_flag_do_not_store, NULL, NULL); tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot, NULL, NULL);
} }
} }

View File

@ -603,10 +603,11 @@ bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_
return result; return result;
} }
bool tf_ssb_db_blob_has(sqlite3* db, const char* id) bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id)
{ {
bool result = false; bool result = false;
sqlite3_stmt* statement; sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1"; const char* query = "SELECT COUNT(*) FROM blobs WHERE id = ?1";
if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, query, -1, &statement, NULL) == SQLITE_OK)
{ {
@ -616,6 +617,7 @@ bool tf_ssb_db_blob_has(sqlite3* db, const char* id)
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
tf_ssb_release_db_reader(ssb, db);
return result; return result;
} }
@ -1679,13 +1681,34 @@ bool tf_ssb_db_set_account_password(uv_loop_t* loop, sqlite3* db, JSContext* con
return result; return result;
} }
static bool _tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool default_value)
{
bool result = default_value;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
result = sqlite3_column_int(statement, 0) != 0;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password) bool tf_ssb_db_register_account(uv_loop_t* loop, sqlite3* db, JSContext* context, const char* name, const char* password)
{ {
bool result = false; bool result = false;
JSValue users_array = JS_UNDEFINED; JSValue users_array = JS_UNDEFINED;
bool registration_allowed = true; bool registration_allowed = _tf_ssb_db_get_global_setting_bool(db, "account_registration", true);
tf_ssb_db_get_global_setting_bool(db, "account_registration", &registration_allowed);
if (registration_allowed) if (registration_allowed)
{ {
sqlite3_stmt* statement = NULL; sqlite3_stmt* statement = NULL;
@ -1900,12 +1923,6 @@ static void _tf_ssb_db_resolve_index_work(tf_ssb_t* ssb, void* user_data)
} }
} }
tf_ssb_release_db_reader(ssb, db); tf_ssb_release_db_reader(ssb, db);
if (!request->path)
{
/* From default global settings. */
request->path = tf_strdup("/~core/ssb/");
}
} }
static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_db_resolve_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -2013,98 +2030,3 @@ bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, c
} }
return has_permission; return has_permission;
} }
bool tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool* out_value)
{
bool result = false;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
*out_value = sqlite3_column_int(statement, 0) != 0;
result = true;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t* out_value)
{
bool result = false;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
*out_value = sqlite3_column_int64(statement, 0);
result = true;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size)
{
bool result = false;
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
snprintf(out_value, size, "%s", sqlite3_column_text(statement, 0));
result = true;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id)
{
const char* result = NULL;
sqlite3_stmt* statement;
if (sqlite3_prepare(db,
"SELECT json(json_group_object(key, value)) FROM (SELECT fields.key, RANK() OVER (PARTITION BY fields.key ORDER BY messages.sequence DESC) AS rank, fields.value FROM "
"messages, json_each(messages.content) AS fields WHERE messages.author = ? AND messages.content ->> '$.type' = 'about' AND messages.content ->> '$.about' = "
"messages.author AND NOT fields.key IN ('about', 'type')) WHERE rank = 1",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
result = tf_strdup((const char*)sqlite3_column_text(statement, 0));
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
return result;
}

View File

@ -39,11 +39,11 @@ bool tf_ssb_db_message_content_get(tf_ssb_t* ssb, const char* id, uint8_t** out_
/** /**
** Determine whether a blob is in the database by ID. ** Determine whether a blob is in the database by ID.
** @param db The SQLite database instance to use. ** @param ssb The SSB instasnce.
** @param id The blob identifier. ** @param id The blob identifier.
** @return true If the blob is in the database. ** @return true If the blob is in the database.
*/ */
bool tf_ssb_db_blob_has(sqlite3* db, const char* id); bool tf_ssb_db_blob_has(tf_ssb_t* ssb, const char* id);
/** /**
** Retrieve a blob from the database. ** Retrieve a blob from the database.
@ -455,42 +455,6 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
*/ */
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, const char* permission); bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, sqlite3* db, const char* id, const char* permission);
/**
** Get a boolean global setting value.
** @param db The database.
** @param name The setting name.
** @param out_value Populated with the value.
** @return true if the setting was found.
*/
bool tf_ssb_db_get_global_setting_bool(sqlite3* db, const char* name, bool* out_value);
/**
** Get an int64_t global setting value.
** @param db The database.
** @param name The setting name.
** @param out_value Populated with the value.
** @return true if the setting was found.
*/
bool tf_ssb_db_get_global_setting_int64(sqlite3* db, const char* name, int64_t* out_value);
/**
** Get a string global setting value.
** @param db The database.
** @param name The setting name.
** @param out_value Populated with the value.
** @param size The size of the out_value buffer.
** @return true if the setting was found.
*/
bool tf_ssb_db_get_global_setting_string(sqlite3* db, const char* name, char* out_value, size_t size);
/**
** Get the latest profile information for the given identity.
** @param db The database.
** @param id The identity.
** @return A JSON representation of the latest profile information set for the account. Free with tf_free().
*/
const char* tf_ssb_db_get_profile(sqlite3* db, const char* id);
/** /**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use. ** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer. ** @param user_data User data registered with the authorizer.

View File

@ -1,322 +0,0 @@
#include "ssb.ebt.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "util.js.h"
#include "uv.h"
#include <string.h>
typedef struct _ebt_entry_t
{
char id[k_id_base64_len];
int64_t out;
int64_t in;
bool out_replicate;
bool out_receive;
bool in_replicate;
bool in_receive;
} ebt_entry_t;
typedef struct _tf_ssb_ebt_t
{
tf_ssb_connection_t* connection;
uv_mutex_t mutex;
ebt_entry_t* entries;
int entries_count;
int send_clock_pending;
} tf_ssb_ebt_t;
tf_ssb_ebt_t* tf_ssb_ebt_create(tf_ssb_connection_t* connection)
{
tf_ssb_ebt_t* ebt = tf_malloc(sizeof(tf_ssb_ebt_t));
*ebt = (tf_ssb_ebt_t) {
.connection = connection,
};
uv_mutex_init(&ebt->mutex);
return ebt;
}
void tf_ssb_ebt_destroy(tf_ssb_ebt_t* ebt)
{
uv_mutex_destroy(&ebt->mutex);
tf_free(ebt->entries);
tf_free(ebt);
}
static int _ebt_entry_compare(const void* a, const void* b)
{
const char* id = a;
const ebt_entry_t* entry = b;
return strcmp(id, entry->id);
}
static ebt_entry_t* _ebt_get_entry(tf_ssb_ebt_t* ebt, const char* id)
{
int index = tf_util_insert_index(id, ebt->entries, ebt->entries_count, sizeof(ebt_entry_t), _ebt_entry_compare);
if (index < ebt->entries_count && strcmp(id, ebt->entries[index].id) == 0)
{
return &ebt->entries[index];
}
else
{
ebt->entries = tf_resize_vec(ebt->entries, (ebt->entries_count + 1) * sizeof(ebt_entry_t));
if (index < ebt->entries_count)
{
memmove(ebt->entries + index + 1, ebt->entries + index, (ebt->entries_count - index) * sizeof(ebt_entry_t));
}
ebt->entries[index] = (ebt_entry_t) {
.in = -1,
.out = -1,
};
snprintf(ebt->entries[index].id, sizeof(ebt->entries[index].id), "%s", id);
ebt->entries_count++;
return &ebt->entries[index];
}
}
void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clock)
{
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, clock, JS_GPN_STRING_MASK) == 0)
{
uv_mutex_lock(&ebt->mutex);
for (uint32_t i = 0; i < plen; ++i)
{
JSValue in_clock = JS_UNDEFINED;
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, clock, ptab[i].atom) == 1)
{
in_clock = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
if (!JS_IsUndefined(in_clock))
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
const char* author = JS_ToCString(context, key);
int64_t sequence = -1;
JS_ToInt64(context, &sequence, in_clock);
ebt_entry_t* entry = _ebt_get_entry(ebt, author);
if (sequence < 0)
{
entry->in = -1;
entry->in_replicate = false;
entry->in_receive = false;
}
else
{
entry->in = sequence >> 1;
entry->in_replicate = true;
entry->in_receive = (sequence & 1) == 0;
}
if (!entry->in_receive)
{
tf_ssb_connection_remove_new_message_request(ebt->connection, author);
}
JS_FreeCString(context, author);
JS_FreeValue(context, key);
}
JS_FreeValue(context, in_clock);
}
uv_mutex_unlock(&ebt->mutex);
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
}
}
typedef struct _ebt_get_clock_t
{
tf_ssb_ebt_t* ebt;
int32_t request_number;
tf_ssb_ebt_clock_callback_t* callback;
tf_ssb_ebt_clock_t* clock;
void* user_data;
} ebt_get_clock_t;
static int _ebt_compare_entry(const void* a, const void* b)
{
const char* id = a;
const tf_ssb_ebt_clock_entry_t* entry = b;
return strcmp(id, entry->id);
}
static void _ebt_add_to_clock(ebt_get_clock_t* work, const char* id, int64_t value, bool replicate, bool receive)
{
int count = work->clock ? work->clock->count : 0;
ebt_entry_t* entry = _ebt_get_entry(work->ebt, id);
if ((replicate && !entry->out_replicate) || (receive && !entry->out_receive) || ((replicate || receive || entry->out_replicate || entry->out_receive) && entry->out != value))
{
entry->out = value;
entry->out_replicate = entry->out_replicate || replicate;
entry->out_receive = entry->out_receive || receive;
int index = tf_util_insert_index(id, count ? work->clock->entries : NULL, count, sizeof(tf_ssb_ebt_clock_entry_t), _ebt_compare_entry);
int64_t out_value = entry->out_replicate ? ((value << 1) | (entry->out_receive ? 0 : 1)) : -1;
if (index < count && strcmp(id, work->clock->entries[index].id) == 0)
{
work->clock->entries[index].value = out_value;
}
else
{
work->clock = tf_resize_vec(work->clock, sizeof(tf_ssb_ebt_clock_t) + (count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
if (index < count)
{
memmove(work->clock->entries + index + 1, work->clock->entries + index, (count - index) * sizeof(tf_ssb_ebt_clock_entry_t));
}
work->clock->entries[index] = (tf_ssb_ebt_clock_entry_t) { .value = out_value };
snprintf(work->clock->entries[index].id, sizeof(work->clock->entries[index].id), "%s", id);
work->clock->count = count + 1;
}
}
}
static void _tf_ssb_ebt_get_send_clock_work(tf_ssb_connection_t* connection, void* user_data)
{
ebt_get_clock_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(work->ebt->connection);
int64_t depth = 2;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "replication_hops", &depth);
tf_ssb_release_db_reader(ssb, db);
/* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
if (visible)
{
int64_t* sequences = NULL;
for (int i = 0; visible[i]; i++)
{
int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, visible[i], &sequence, NULL, 0);
sequences = tf_resize_vec(sequences, (i + 1) * sizeof(int64_t));
sequences[i] = sequence;
}
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; visible[i]; i++)
{
_ebt_add_to_clock(work, visible[i], sequences[i], true, true);
}
uv_mutex_unlock(&work->ebt->mutex);
tf_free(visible);
tf_free(sequences);
}
/* Ask about the incoming connection, too. */
char id[k_id_base64_len] = "";
if (tf_ssb_connection_get_id(connection, id, sizeof(id)))
{
int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
uv_mutex_lock(&work->ebt->mutex);
_ebt_add_to_clock(work, id, sequence, true, true);
uv_mutex_unlock(&work->ebt->mutex);
}
/* Also respond with what we know about all requested identities. */
tf_ssb_ebt_clock_entry_t* requested = NULL;
int requested_count = 0;
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; i < work->ebt->entries_count; i++)
{
ebt_entry_t* entry = &work->ebt->entries[i];
if (entry->in_replicate && !entry->out_replicate)
{
requested = tf_resize_vec(requested, (requested_count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
requested[requested_count] = (tf_ssb_ebt_clock_entry_t) { .value = -1 };
snprintf(requested[requested_count].id, sizeof(requested[requested_count].id), "%s", entry->id);
requested_count++;
}
}
uv_mutex_unlock(&work->ebt->mutex);
if (requested_count)
{
for (int i = 0; i < requested_count; i++)
{
tf_ssb_db_get_latest_message_by_author(ssb, requested[i].id, &requested[i].value, NULL, 0);
}
uv_mutex_lock(&work->ebt->mutex);
for (int i = 0; i < requested_count; i++)
{
_ebt_add_to_clock(work, requested[i].id, requested[i].value, requested[i].value >= 0, false);
}
uv_mutex_unlock(&work->ebt->mutex);
tf_free(requested);
}
}
static void _tf_ssb_ebt_get_send_clock_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
{
ebt_get_clock_t* work = user_data;
work->callback(work->clock, work->request_number, work->user_data);
tf_free(work->clock);
tf_free(work);
}
void tf_ssb_ebt_get_send_clock(tf_ssb_ebt_t* ebt, int32_t request_number, tf_ssb_ebt_clock_callback_t* callback, void* user_data)
{
ebt_get_clock_t* work = tf_malloc(sizeof(ebt_get_clock_t));
*work = (ebt_get_clock_t) {
.ebt = ebt,
.request_number = request_number,
.callback = callback,
.user_data = user_data,
};
tf_ssb_connection_run_work(ebt->connection, _tf_ssb_ebt_get_send_clock_work, _tf_ssb_ebt_get_send_clock_after_work, work);
}
tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt)
{
int count = 0;
tf_ssb_ebt_clock_t* clock = NULL;
uv_mutex_lock(&ebt->mutex);
for (int i = 0; i < ebt->entries_count; i++)
{
ebt_entry_t* entry = &ebt->entries[i];
if (entry->in_replicate && entry->in_receive && entry->out > entry->in)
{
clock = tf_resize_vec(clock, sizeof(tf_ssb_ebt_clock_t) + (count + 1) * sizeof(tf_ssb_ebt_clock_entry_t));
clock->entries[count] = (tf_ssb_ebt_clock_entry_t) { .value = entry->in };
snprintf(clock->entries[count].id, sizeof(clock->entries[count].id), "%s", entry->id);
clock->count = ++count;
}
}
uv_mutex_unlock(&ebt->mutex);
return clock;
}
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence)
{
uv_mutex_lock(&ebt->mutex);
ebt_entry_t* entry = _ebt_get_entry(ebt, id);
entry->in = tf_max(entry->in, sequence);
if (entry->in == entry->out && (tf_ssb_connection_get_flags(ebt->connection) & k_tf_ssb_connect_flag_one_shot) == 0)
{
tf_ssb_connection_add_new_message_request(ebt->connection, id, tf_ssb_connection_get_ebt_request_number(ebt->connection), false);
}
uv_mutex_unlock(&ebt->mutex);
}
int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt)
{
return ebt->send_clock_pending;
}
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending)
{
ebt->send_clock_pending = pending;
}

View File

@ -1,99 +0,0 @@
#pragma once
#include "ssb.h"
#include "quickjs.h"
typedef struct _tf_ssb_connection_t tf_ssb_connection_t;
/**
** SSB EBT state.
*/
typedef struct _tf_ssb_ebt_t tf_ssb_ebt_t;
/**
** An EBT clock entry (identity + sequence pair).
*/
typedef struct _tf_ssb_ebt_clock_entry_t
{
/** The identity. */
char id[k_id_base64_len];
/** The sequence number. */
int64_t value;
} tf_ssb_ebt_clock_entry_t;
/**
** A set of IDs and sequence values.
*/
typedef struct _tf_ssb_ebt_clock_t
{
/** Number of entries. */
int count;
/** Clock entries. */
tf_ssb_ebt_clock_entry_t entries[];
} tf_ssb_ebt_clock_t;
/**
** A callback with EBT clock state.
*/
typedef void(tf_ssb_ebt_clock_callback_t)(const tf_ssb_ebt_clock_t* clock, int32_t request_number, void* user_data);
/**
** Create an EBT instance.
** @param connection The SSB connection to which this EBT state applies.
** @return The EBT instance.
*/
tf_ssb_ebt_t* tf_ssb_ebt_create(tf_ssb_connection_t* connection);
/**
** Update the EBT state with a received clock.
** @param ebt The EBT instance.
** @param context The JS context.
** @param clock The received clock.
*/
void tf_ssb_ebt_receive_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue clock);
/**
** Get the EBT clock state to send.
** @param ebt The EBT instance.
** @param request_number The request number for which the clock will be sent.
** @param callback Called with the clock when determined.
** @param user_data User data passed to the callback.
*/
void tf_ssb_ebt_get_send_clock(tf_ssb_ebt_t* ebt, int32_t request_number, tf_ssb_ebt_clock_callback_t* callback, void* user_data);
/**
** Get the set of messages requested to be sent.
** @param ebt The EBT instance.
** @return A clock of identities and sequence numbers indicating which messages
** are due to be sent. The caller must free with tf_free().
*/
tf_ssb_ebt_clock_t* tf_ssb_ebt_get_messages_to_send(tf_ssb_ebt_t* ebt);
/**
** Update the clock state indicating the messages that have been sent for an account.
** @param ebt The EBT instance.
** @param id The identity to update.
** @param sequence The maximum sequence number sent.
*/
void tf_ssb_ebt_set_messages_sent(tf_ssb_ebt_t* ebt, const char* id, int64_t sequence);
/**
** Destroy an EBT instance.
** @param ebt The EBT instance.
*/
void tf_ssb_ebt_destroy(tf_ssb_ebt_t* ebt);
/**
** Get whether sending the clock is pending.
** @param ebt The EBT instance.
** @return The last value set by tf_ssb_ebt_set_send_clock_pending().
*/
int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt);
/**
** Set whether sending the clock is pending.
** @param ebt The EBT instance.
** @param pending A value representing the pending status.
*/
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending);

View File

@ -71,15 +71,12 @@ typedef enum _tf_ssb_message_flags_t
typedef enum _tf_ssb_connect_flags_t typedef enum _tf_ssb_connect_flags_t
{ {
k_tf_ssb_connect_flag_one_shot = 0x1, k_tf_ssb_connect_flag_one_shot = 0x1,
k_tf_ssb_connect_flag_do_not_store = 0x2,
} tf_ssb_connect_flags_t; } tf_ssb_connect_flags_t;
/** An SSB instance. */ /** An SSB instance. */
typedef struct _tf_ssb_t tf_ssb_t; typedef struct _tf_ssb_t tf_ssb_t;
/** An SSB connection. */ /** An SSB connection. */
typedef struct _tf_ssb_connection_t tf_ssb_connection_t; typedef struct _tf_ssb_connection_t tf_ssb_connection_t;
/** A connection's EBT state. */
typedef struct _tf_ssb_ebt_t tf_ssb_ebt_t;
/** A trace instance. */ /** A trace instance. */
typedef struct _tf_trace_t tf_trace_t; typedef struct _tf_trace_t tf_trace_t;
/** An SQLite database handle. */ /** An SQLite database handle. */
@ -517,9 +514,8 @@ JSContext* tf_ssb_connection_get_context(tf_ssb_connection_t* connection);
/** /**
** Close a connection. ** Close a connection.
** @param connection The connection. ** @param connection The connection.
** @param reason Human-readable reason for closing the connection.
*/ */
void tf_ssb_connection_close(tf_ssb_connection_t* connection, const char* reason); void tf_ssb_connection_close(tf_ssb_connection_t* connection);
/** /**
** Check whether a connection is connected. ** Check whether a connection is connected.
@ -528,13 +524,6 @@ void tf_ssb_connection_close(tf_ssb_connection_t* connection, const char* reason
*/ */
bool tf_ssb_connection_is_connected(tf_ssb_connection_t* connection); bool tf_ssb_connection_is_connected(tf_ssb_connection_t* connection);
/**
** Check whether a connection is in the process of closing.
** @param connection The connection.
** @return True if the connection is closing.
*/
bool tf_ssb_connection_is_closing(tf_ssb_connection_t* connection);
/** /**
** Get the next outgoing request number for a connection. ** Get the next outgoing request number for a connection.
** @param connection The connection. ** @param connection The connection.
@ -809,19 +798,17 @@ JSValue tf_ssb_connection_requests_to_object(tf_ssb_connection_t* connection);
/** /**
** A function scheduled to be run later. ** A function scheduled to be run later.
** @param connection The owning connection. ** @param connection The owning connection.
** @param skip Whether the work ought to be skipped, because it is being replaced.
** @param user_data User data registered with the callback. ** @param user_data User data registered with the callback.
*/ */
typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, bool skip, void* user_data); typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, void* user_data);
/** /**
** Schedule work to be run when the connection is next idle. ** Schedule work to be run when the connection is next idle.
** @param connection The owning connection. ** @param connection The owning connection.
** @param key A key identifying the work. If work by the same key already exists, the new request will be discarded.
** @param callback The callback to call. ** @param callback The callback to call.
** @param user_data User data to pass to the callback. ** @param user_data User data to pass to the callback.
*/ */
void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, const char* key, tf_ssb_scheduled_callback_t* callback, void* user_data); void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, tf_ssb_scheduled_callback_t* callback, void* user_data);
/** /**
** Schedule work to run on a worker thread. ** Schedule work to run on a worker thread.
@ -926,6 +913,20 @@ int32_t tf_ssb_connection_get_ebt_request_number(tf_ssb_connection_t* connection
*/ */
void tf_ssb_connection_set_ebt_request_number(tf_ssb_connection_t* connection, int32_t request_number); void tf_ssb_connection_set_ebt_request_number(tf_ssb_connection_t* connection, int32_t request_number);
/**
** Get whether the EBT clock has been sent for a connection.
** @param connection An SHS connection.
** @return True if the clock has been sent.
*/
bool tf_ssb_connection_get_sent_clock(tf_ssb_connection_t* connection);
/**
** Set the EBT clock sent state for a connection.
** @param connection An SHS connection.
** @param sent_clock Whether the clock has been sent.
*/
void tf_ssb_connection_set_sent_clock(tf_ssb_connection_t* connection, bool sent_clock);
/** /**
** Get the JS class ID of the SSB connection class. ** Get the JS class ID of the SSB connection class.
** @return The class ID ** @return The class ID
@ -946,6 +947,14 @@ void tf_ssb_get_stats(tf_ssb_t* ssb, tf_ssb_stats_t* out_stats);
*/ */
tf_ssb_blob_wants_t* tf_ssb_connection_get_blob_wants_state(tf_ssb_connection_t* connection); tf_ssb_blob_wants_t* tf_ssb_connection_get_blob_wants_state(tf_ssb_connection_t* connection);
/**
** Get a report of information about recent disconnections.
** @param ssb The SSB instance.
** @param context A JS context.
** @return Information about disconnections.
*/
JSValue tf_ssb_get_disconnection_debug(tf_ssb_t* ssb, JSContext* context);
/** /**
** Record whether the calling thread is busy. ** Record whether the calling thread is busy.
** @param ssb The SSB instance. ** @param ssb The SSB instance.
@ -1092,13 +1101,6 @@ void tf_ssb_connection_adjust_read_backpressure(tf_ssb_connection_t* connection,
*/ */
void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta); void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int delta);
/**
** Get the reason why a connection is going away.
** @param connection The connection.
** @return The reason or NULL.
*/
const char* tf_ssb_connection_get_destroy_reason(tf_ssb_connection_t* connection);
/** /**
** Initiate a tunnel connection. ** Initiate a tunnel connection.
** @param ssb The SSB instance. ** @param ssb The SSB instance.
@ -1121,11 +1123,4 @@ void tf_ssb_sync_start(tf_ssb_t* ssb);
*/ */
int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection); int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection);
/**
** Get a connection's EBT state.
** @param connection The connection.
** @return the EBT state for the connection.
*/
tf_ssb_ebt_t* tf_ssb_connection_get_ebt(tf_ssb_connection_t* connection);
/** @} */ /** @} */

View File

@ -251,6 +251,111 @@ static JSValue _tf_ssb_deleteIdentity(JSContext* context, JSValueConst this_val,
return result; return result;
} }
static JSValue _set_server_following_internal(tf_ssb_t* ssb, JSValueConst this_val, const char* id, bool follow)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue message = JS_NewObject(context);
JSValue server_user = JS_NewString(context, ":admin");
char server_id_buffer[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, server_id_buffer, sizeof(server_id_buffer));
JSValue server_id = JS_NewString(context, server_id_buffer);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "contact"));
JS_SetPropertyStr(context, message, "contact", JS_NewString(context, id));
JS_SetPropertyStr(context, message, "following", JS_NewBool(context, follow));
JSValue args[] = {
server_user,
server_id,
message,
};
JSValue result = _tf_ssb_appendMessageWithIdentity(context, this_val, tf_countof(args), args);
JS_FreeValue(context, server_id);
JS_FreeValue(context, server_user);
JS_FreeValue(context, message);
return result;
}
typedef struct _set_server_following_me_t
{
const char* user;
const char* key;
bool follow;
JSValue this_val;
JSValue promise[2];
bool error_does_not_own_key;
bool append_message;
} set_server_following_me_t;
static void _tf_ssb_set_server_following_me_work(tf_ssb_t* ssb, void* user_data)
{
set_server_following_me_t* work = user_data;
if (!tf_ssb_db_identity_get_private_key(ssb, work->user, work->key, NULL, 0))
{
work->error_does_not_own_key = true;
}
else
{
char server_id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, server_id, sizeof(server_id));
const char* server_id_ptr = server_id;
const char** current_following = tf_ssb_db_following_deep_ids(ssb, &server_id_ptr, 1, 1);
bool is_following = false;
for (const char** it = current_following; *it; it++)
{
if (strcmp(work->key, *it) == 0)
{
is_following = true;
break;
}
}
tf_free(current_following);
work->append_message = (work->follow && !is_following) || (!work->follow && is_following);
}
}
static void _tf_ssb_set_server_following_me_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
set_server_following_me_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue result = JS_UNDEFINED;
if (work->error_does_not_own_key)
{
result = JS_ThrowInternalError(context, "User %s does not own key %s.", work->user, work->key);
}
else if (work->append_message)
{
result = _set_server_following_internal(ssb, work->this_val, work->key, work->follow);
}
JS_FreeCString(context, work->key);
JS_FreeCString(context, work->user);
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
JS_FreeValue(context, result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->this_val);
tf_free(work);
}
static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
JSValue result = JS_UNDEFINED;
if (ssb)
{
set_server_following_me_t* work = tf_malloc(sizeof(set_server_following_me_t));
*work = (set_server_following_me_t) {
.user = JS_ToCString(context, argv[0]),
.key = JS_ToCString(context, argv[1]),
.follow = JS_ToBool(context, argv[2]),
.this_val = JS_DupValue(context, this_val),
};
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_set_server_following_me_work, _tf_ssb_set_server_following_me_after_work, work);
}
return result;
}
typedef struct _swap_with_server_identity_t typedef struct _swap_with_server_identity_t
{ {
char server_id[k_id_base64_len]; char server_id[k_id_base64_len];
@ -1015,11 +1120,6 @@ static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, in
int flags = tf_ssb_connection_get_flags(connection); int flags = tf_ssb_connection_get_flags(connection);
JS_SetPropertyStr(context, flags_object, "one_shot", JS_NewBool(context, (flags & k_tf_ssb_connect_flag_one_shot) != 0)); JS_SetPropertyStr(context, flags_object, "one_shot", JS_NewBool(context, (flags & k_tf_ssb_connect_flag_one_shot) != 0));
JS_SetPropertyStr(context, object, "flags", flags_object); JS_SetPropertyStr(context, object, "flags", flags_object);
const char* destroy_reason = tf_ssb_connection_get_destroy_reason(connection);
if (destroy_reason)
{
JS_SetPropertyStr(context, object, "destroy_reason", JS_NewString(context, destroy_reason));
}
JS_SetPropertyUint32(context, result, i, object); JS_SetPropertyUint32(context, result, i, object);
} }
} }
@ -1093,7 +1193,7 @@ static JSValue _tf_ssb_closeConnection(JSContext* context, JSValueConst this_val
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, id); tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, id);
if (connection) if (connection)
{ {
tf_ssb_connection_close(connection, "Closed by user"); tf_ssb_connection_close(connection);
} }
JS_FreeCString(context, id); JS_FreeCString(context, id);
return connection ? JS_TRUE : JS_FALSE; return connection ? JS_TRUE : JS_FALSE;
@ -1390,10 +1490,10 @@ static JSValue _tf_ssb_sqlAsync(JSContext* context, JSValueConst this_val, int a
uv_mutex_init(&work->lock); uv_mutex_init(&work->lock);
uv_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_sqlAsync_start_timer); uv_async_init(tf_ssb_get_loop(ssb), &work->async, _tf_ssb_sqlAsync_start_timer);
uv_timer_init(tf_ssb_get_loop(ssb), &work->timeout); uv_timer_init(tf_ssb_get_loop(ssb), &work->timeout);
JSValue result = JS_UNDEFINED; JSValue result = JS_NewPromiseCapability(context, work->promise);
JSValue error_value = JS_UNDEFINED;
if (ssb) if (ssb)
{ {
result = JS_NewPromiseCapability(context, work->promise);
int32_t length = tf_util_get_length(context, argv[1]); int32_t length = tf_util_get_length(context, argv[1]);
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
@ -1436,6 +1536,18 @@ static JSValue _tf_ssb_sqlAsync(JSContext* context, JSValueConst this_val, int a
} }
tf_ssb_run_work(ssb, _tf_ssb_sqlAsync_work, _tf_ssb_sqlAsync_after_work, work); tf_ssb_run_work(ssb, _tf_ssb_sqlAsync_work, _tf_ssb_sqlAsync_after_work, work);
} }
if (!JS_IsUndefined(error_value))
{
JSValue call_result = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &error_value);
tf_util_report_error(context, call_result);
JS_FreeValue(context, call_result);
JS_FreeValue(context, error_value);
JS_FreeCString(context, query);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
JS_FreeValue(context, work->callback);
_tf_ssb_sqlAsync_destroy(work);
}
return result; return result;
} }
@ -2122,60 +2234,53 @@ static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
if (found) if (found)
{ {
if (work->message_size >= strlen(".box") && memcmp(work->message + work->message_size - strlen(".box"), ".box", strlen(".box")) == 0) uint8_t* decoded = tf_malloc(work->message_size);
int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size);
uint8_t* nonce = decoded;
uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
{ {
uint8_t* decoded = tf_malloc(work->message_size); uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
int decoded_length = tf_base64_decode(work->message, work->message_size - strlen(".box"), decoded, work->message_size); if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
uint8_t* nonce = decoded;
uint8_t* public_key = decoded + crypto_box_NONCEBYTES;
if (public_key + crypto_secretbox_KEYBYTES < decoded + decoded_length)
{ {
uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 }; enum
if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
{ {
enum k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
};
for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes;
p += k_recipient_header_bytes)
{
uint8_t out[k_recipient_header_bytes] = { 0 };
int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
if (opened != -1)
{ {
k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES int recipients = (int)out[0];
}; uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes; size_t body_size = decoded + decoded_length - body;
p += k_recipient_header_bytes) uint8_t* decrypted = tf_malloc(body_size);
{ uint8_t* key = out + 1;
uint8_t out[k_recipient_header_bytes] = { 0 }; if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
int opened = crypto_secretbox_open_easy(out, p, k_recipient_header_bytes, nonce, shared_secret);
if (opened != -1)
{ {
int recipients = (int)out[0]; work->decrypted = (const char*)decrypted;
uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients; work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
size_t body_size = decoded + decoded_length - body; }
uint8_t* decrypted = tf_malloc(body_size); else
uint8_t* key = out + 1; {
if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1) work->error = "Received key to open secret box containing message body, but it did not work.";
{
work->decrypted = (const char*)decrypted;
work->decrypted_size = body_size - crypto_secretbox_MACBYTES;
}
else
{
work->error = "Received key to open secret box containing message body, but it did not work.";
}
} }
} }
} }
else
{
work->error = "crypto_scalarmult failed.";
}
} }
else else
{ {
work->error = "Encrypted message was not long enough to contain its one-time public key."; work->error = "crypto_scalarmult failed.";
} }
tf_free(decoded);
} }
else else
{ {
work->error = "Message does not end in \".box\"."; work->error = "Encrypted message was not long enough to contains its one-time public key.";
} }
tf_free(decoded);
} }
else else
{ {
@ -2457,6 +2562,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1)); JS_SetPropertyStr(context, object, "createIdentity", JS_NewCFunction(context, _tf_ssb_createIdentity, "createIdentity", 1));
JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2)); JS_SetPropertyStr(context, object, "addIdentity", JS_NewCFunction(context, _tf_ssb_addIdentity, "addIdentity", 2));
JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2)); JS_SetPropertyStr(context, object, "deleteIdentity", JS_NewCFunction(context, _tf_ssb_deleteIdentity, "deleteIdentity", 2));
JS_SetPropertyStr(context, object, "setServerFollowingMe", JS_NewCFunction(context, _tf_ssb_set_server_following_me, "setServerFollowingMe", 3));
JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2)); JS_SetPropertyStr(context, object, "swapWithServerIdentity", JS_NewCFunction(context, _tf_ssb_swap_with_server_identity, "swapWithServerIdentity", 2));
JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1)); JS_SetPropertyStr(context, object, "getIdentities", JS_NewCFunction(context, _tf_ssb_getIdentities, "getIdentities", 1));
JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2)); JS_SetPropertyStr(context, object, "getPrivateKey", JS_NewCFunction(context, _tf_ssb_getPrivateKey, "getPrivateKey", 2));

View File

@ -3,7 +3,6 @@
#include "log.h" #include "log.h"
#include "mem.h" #include "mem.h"
#include "ssb.db.h" #include "ssb.db.h"
#include "ssb.ebt.h"
#include "ssb.h" #include "ssb.h"
#include "util.js.h" #include "util.js.h"
@ -20,7 +19,54 @@ static void _tf_ssb_connection_send_history_stream(
static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection); static void _tf_ssb_rpc_send_peers_exchange(tf_ssb_connection_t* connection);
static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms); static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms);
static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms); static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms);
static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data);
static int64_t _get_global_setting_int64(tf_ssb_t* ssb, const char* name, int64_t default_value)
{
int64_t result = default_value;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_type(statement, 0) != SQLITE_NULL)
{
result = sqlite3_column_int64(statement, 0);
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
static bool _get_global_setting_bool(tf_ssb_t* ssb, const char* name, bool default_value)
{
bool result = default_value;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT json_extract(value, '$.' || ?) FROM properties WHERE id = 'core' AND key = 'settings'", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, name, -1, NULL) == SQLITE_OK)
{
if (sqlite3_step(statement) == SQLITE_ROW)
{
result = sqlite3_column_int(statement, 0) != 0;
}
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
}
tf_ssb_release_db_reader(ssb, db);
return result;
}
static void _tf_ssb_rpc_gossip_ping_callback( static void _tf_ssb_rpc_gossip_ping_callback(
tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
@ -139,9 +185,7 @@ static void _tf_ssb_rpc_blobs_has_work(tf_ssb_connection_t* connection, void* us
{ {
blobs_has_work_t* work = user_data; blobs_has_work_t* work = user_data;
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
sqlite3* db = tf_ssb_acquire_db_reader(ssb); work->found = tf_ssb_db_blob_has(ssb, work->id);
work->found = tf_ssb_db_blob_has(db, work->id);
tf_ssb_release_db_reader(ssb, db);
} }
static void _tf_ssb_rpc_blobs_has_after_work(tf_ssb_connection_t* connection, int status, void* user_data) static void _tf_ssb_rpc_blobs_has_after_work(tf_ssb_connection_t* connection, int status, void* user_data)
@ -199,10 +243,7 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
blob_wants_work_t* work = user_data; blob_wants_work_t* work = user_data;
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection); tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
int64_t age = -1; int64_t age = _get_global_setting_int64(ssb, "blob_fetch_age_seconds", -1);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "blob_fetch_age_seconds", &age);
tf_ssb_release_db_reader(ssb, db);
int64_t timestamp = -1; int64_t timestamp = -1;
if (age == 0) if (age == 0)
{ {
@ -215,7 +256,7 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
timestamp = now - age * 1000ULL; timestamp = now - age * 1000ULL;
} }
db = tf_ssb_acquire_db_reader(ssb); sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement; sqlite3_stmt* statement;
if (sqlite3_prepare(db, "SELECT id FROM blob_wants_view WHERE id > ? AND timestamp > ? ORDER BY id LIMIT ?", -1, &statement, NULL) == SQLITE_OK) if (sqlite3_prepare(db, "SELECT id FROM blob_wants_view WHERE id > ? AND timestamp > ? ORDER BY id LIMIT ?", -1, &statement, NULL) == SQLITE_OK)
{ {
@ -295,29 +336,7 @@ static void _tf_ssb_rpc_tunnel_callback(tf_ssb_connection_t* connection, uint8_t
if (flags & k_ssb_rpc_flag_end_error) if (flags & k_ssb_rpc_flag_end_error)
{ {
tf_ssb_connection_remove_request(connection, request_number); tf_ssb_connection_remove_request(connection, request_number);
tf_ssb_connection_close(tun->connection);
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message_val = JS_GetPropertyStr(context, args, "message");
JSValue stack_val = JS_GetPropertyStr(context, args, "stack");
char buffer[1024];
if (!JS_IsUndefined(message_val))
{
const char* message_string = JS_ToCString(context, message_val);
const char* stack_string = JS_ToCString(context, stack_val);
snprintf(buffer, sizeof(buffer), "Error from tunnel: %s\n%s", message_string, stack_string);
JS_FreeCString(context, message_string);
JS_FreeCString(context, stack_string);
}
else
{
snprintf(buffer, sizeof(buffer), "Error from tunnel: %.*s", (int)size, message);
}
JS_FreeValue(context, stack_val);
JS_FreeValue(context, message_val);
tf_ssb_connection_close(tun->connection, buffer);
} }
else else
{ {
@ -855,21 +874,11 @@ static void _tf_ssb_connection_send_history_stream_work(tf_ssb_connection_t* con
request->out_finished = request->out_max_sequence_seen != request->sequence + k_max - 1; request->out_finished = request->out_max_sequence_seen != request->sequence + k_max - 1;
} }
static void _tf_ssb_connection_send_history_stream_destroy(tf_ssb_connection_send_history_stream_t* request)
{
for (int i = 0; i < request->out_messages_count; i++)
{
tf_free(request->out_messages[i]);
}
tf_free(request->out_messages);
tf_free(request);
}
static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_t* connection, int result, void* user_data) static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{ {
tf_ssb_connection_send_history_stream_t* request = user_data; tf_ssb_connection_send_history_stream_t* request = user_data;
tf_ssb_connection_adjust_write_count(connection, -1); tf_ssb_connection_adjust_write_count(connection, -1);
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection))) if (tf_ssb_connection_is_connected(connection))
{ {
for (int i = 0; i < request->out_messages_count; i++) for (int i = 0; i < request->out_messages_count; i++)
{ {
@ -879,7 +888,6 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
break; break;
} }
} }
tf_ssb_ebt_set_messages_sent(tf_ssb_connection_get_ebt(connection), request->author, request->out_max_sequence_seen);
if (!request->out_finished) if (!request->out_finished)
{ {
_tf_ssb_connection_send_history_stream( _tf_ssb_connection_send_history_stream(
@ -890,40 +898,40 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json, request->request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL); tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json, request->request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
} }
} }
_tf_ssb_connection_send_history_stream_destroy(request); for (int i = 0; i < request->out_messages_count; i++)
{
tf_free(request->out_messages[i]);
}
tf_free(request->out_messages);
tf_free(request);
} }
static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t* connection, bool skip, void* user_data) static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t* connection, void* user_data)
{ {
tf_ssb_connection_adjust_write_count(connection, 1); tf_ssb_connection_adjust_write_count(connection, 1);
if (!skip && tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection))) if (tf_ssb_connection_is_connected(connection))
{ {
tf_ssb_connection_run_work(connection, _tf_ssb_connection_send_history_stream_work, _tf_ssb_connection_send_history_stream_after_work, user_data); tf_ssb_connection_run_work(connection, _tf_ssb_connection_send_history_stream_work, _tf_ssb_connection_send_history_stream_after_work, user_data);
} }
else else
{ {
_tf_ssb_connection_send_history_stream_destroy(user_data); _tf_ssb_connection_send_history_stream_after_work(connection, -1, user_data);
} }
} }
static void _tf_ssb_connection_send_history_stream( static void _tf_ssb_connection_send_history_stream(
tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live, bool end_request) tf_ssb_connection_t* connection, int32_t request_number, const char* author, int64_t sequence, bool keys, bool live, bool end_request)
{ {
if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection)) tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t));
{ *async = (tf_ssb_connection_send_history_stream_t) {
tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t)); .request_number = request_number,
*async = (tf_ssb_connection_send_history_stream_t) { .sequence = sequence,
.request_number = request_number, .keys = keys,
.sequence = sequence, .live = live,
.keys = keys, .end_request = end_request,
.live = live, };
.end_request = end_request, snprintf(async->author, sizeof(async->author), "%s", author);
}; tf_ssb_connection_schedule_idle(connection, _tf_ssb_connection_send_history_stream_callback, async);
snprintf(async->author, sizeof(async->author), "%s", author);
char key[128];
snprintf(key, sizeof(key), "%s:%" PRId64, author, sequence);
tf_ssb_connection_schedule_idle(connection, key, _tf_ssb_connection_send_history_stream_callback, async);
}
} }
static void _tf_ssb_rpc_createHistoryStream( static void _tf_ssb_rpc_createHistoryStream(
@ -968,20 +976,194 @@ static void _tf_ssb_rpc_createHistoryStream(
JS_FreeValue(context, arg_array); JS_FreeValue(context, arg_array);
} }
static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connection) typedef struct _ebt_clock_row_t
{ {
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection); char id[k_id_base64_len];
tf_ssb_ebt_clock_t* clock = tf_ssb_ebt_get_messages_to_send(ebt); int64_t value;
if (clock) } ebt_clock_row_t;
typedef struct _ebt_replicate_send_clock_t
{
int64_t request_number;
ebt_clock_row_t* clock;
int clock_count;
char* out_clock;
} ebt_replicate_send_clock_t;
static void _tf_ssb_rpc_ebt_replicate_send_clock_work(tf_ssb_connection_t* connection, void* user_data)
{
ebt_replicate_send_clock_t* work = user_data;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSValue full_clock = JS_NewObject(context);
int64_t depth = _get_global_setting_int64(ssb, "replication_hops", 2);
/* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth);
for (int i = 0; visible[i]; i++)
{ {
for (int i = 0; i < clock->count; i++) int64_t sequence = 0;
tf_ssb_db_get_latest_message_by_author(ssb, visible[i], &sequence, NULL, 0);
JS_SetPropertyStr(context, full_clock, visible[i], JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
}
tf_free(visible);
/* Ask about the incoming connection, too. */
char id[k_id_base64_len] = "";
if (tf_ssb_connection_get_id(connection, id, sizeof(id)))
{
JSValue in_clock = JS_GetPropertyStr(context, full_clock, id);
if (JS_IsUndefined(in_clock))
{ {
tf_ssb_ebt_clock_entry_t* entry = &clock->entries[i]; int64_t sequence = 0;
int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection); tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0);
bool live = (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_one_shot) == 0; JS_SetPropertyStr(context, full_clock, id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
_tf_ssb_connection_send_history_stream(connection, request_number, entry->id, entry->value, false, live, false);
} }
tf_free(clock); JS_FreeValue(context, in_clock);
}
/* Also respond with what we know about all requested identities. */
for (int i = 0; i < work->clock_count; i++)
{
JSValue in_clock = JS_GetPropertyStr(context, full_clock, work->clock[i].id);
if (JS_IsUndefined(in_clock))
{
int64_t sequence = -1;
tf_ssb_db_get_latest_message_by_author(ssb, work->clock[i].id, &sequence, NULL, 0);
JS_SetPropertyStr(context, full_clock, work->clock[i].id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1)));
}
JS_FreeValue(context, in_clock);
}
JSValue json = JS_JSONStringify(context, full_clock, JS_NULL, JS_NULL);
size_t size = 0;
const char* string = JS_ToCStringLen(context, &size, json);
char* copy = tf_malloc(size + 1);
memcpy(copy, string, size + 1);
work->out_clock = copy;
JS_FreeCString(context, string);
JS_FreeValue(context, json);
JS_FreeValue(context, full_clock);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _tf_ssb_rpc_ebt_replicate_send_clock_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{
ebt_replicate_send_clock_t* work = user_data;
tf_free(work->clock);
if (work->out_clock)
{
tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, -work->request_number, NULL, (const uint8_t*)work->out_clock, strlen(work->out_clock), NULL, NULL, NULL);
tf_free(work->out_clock);
}
tf_free(work);
}
static void _tf_ssb_rpc_ebt_replicate_send_clock(tf_ssb_connection_t* connection, int32_t request_number, JSValue message)
{
ebt_replicate_send_clock_t* work = tf_malloc(sizeof(ebt_replicate_send_clock_t));
*work = (ebt_replicate_send_clock_t) {
.request_number = request_number,
};
JSContext* context = tf_ssb_connection_get_context(connection);
if (!JS_IsUndefined(message))
{
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, message, JS_GPN_STRING_MASK) == 0)
{
work->clock_count = (int)plen;
work->clock = tf_malloc(sizeof(ebt_clock_row_t) * plen);
memset(work->clock, 0, sizeof(ebt_clock_row_t) * plen);
for (uint32_t i = 0; i < plen; ++i)
{
const char* id = JS_AtomToCString(context, ptab[i].atom);
snprintf(work->clock[i].id, sizeof(work->clock[i].id), "%s", id);
JS_FreeCString(context, id);
JSPropertyDescriptor desc = { 0 };
JSValue key_value = JS_UNDEFINED;
if (JS_GetOwnProperty(context, &desc, message, ptab[i].atom) == 1)
{
key_value = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
JS_ToInt64(context, &work->clock[i].value, key_value);
JS_FreeValue(context, key_value);
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
}
}
tf_ssb_connection_run_work(connection, _tf_ssb_rpc_ebt_replicate_send_clock_work, _tf_ssb_rpc_ebt_replicate_send_clock_after_work, work);
}
static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connection, JSValue message)
{
if (JS_IsUndefined(message))
{
return;
}
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSContext* context = tf_ssb_get_context(ssb);
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
if (JS_GetOwnPropertyNames(context, &ptab, &plen, message, JS_GPN_STRING_MASK) == 0)
{
for (uint32_t i = 0; i < plen; ++i)
{
JSValue in_clock = JS_UNDEFINED;
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, message, ptab[i].atom) == 1)
{
in_clock = desc.value;
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
}
if (!JS_IsUndefined(in_clock))
{
JSValue key = JS_AtomToString(context, ptab[i].atom);
int64_t sequence = -1;
JS_ToInt64(context, &sequence, in_clock);
const char* author = JS_ToCString(context, key);
if (sequence >= 0 && (sequence & 1) == 0)
{
int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection);
bool live = (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_one_shot) == 0;
_tf_ssb_connection_send_history_stream(connection, request_number, author, sequence >> 1, false, live, false);
if (live)
{
tf_ssb_connection_add_new_message_request(connection, author, request_number, false);
}
}
else
{
tf_ssb_connection_remove_new_message_request(connection, author);
}
JS_FreeCString(context, author);
JS_FreeValue(context, key);
}
JS_FreeValue(context, in_clock);
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
} }
} }
@ -995,54 +1177,14 @@ typedef struct _resend_clock_t
{ {
tf_ssb_connection_t* connection; tf_ssb_connection_t* connection;
int32_t request_number; int32_t request_number;
int pending;
} resend_clock_t; } resend_clock_t;
static void _tf_ssb_rpc_ebt_send_clock_callback(const tf_ssb_ebt_clock_t* clock, int32_t request_number, void* user_data) static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, void* user_data)
{ {
resend_clock_t* resend = user_data; resend_clock_t* resend = user_data;
tf_ssb_connection_t* connection = resend->connection; _tf_ssb_rpc_ebt_replicate_send_clock(resend->connection, resend->request_number, JS_UNDEFINED);
tf_ssb_connection_set_sent_clock(resend->connection, true);
if (clock && clock->count) tf_free(user_data);
{
JSContext* context = tf_ssb_connection_get_context(connection);
JSValue message = JS_NewObject(context);
for (int i = 0; i < clock->count; i++)
{
JS_SetPropertyStr(context, message, clock->entries[i].id, JS_NewInt64(context, clock->entries[i].value));
}
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, -request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
}
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
if (resend->pending != tf_ssb_ebt_get_send_clock_pending(ebt) && tf_ssb_connection_is_connected(connection) &&
!tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
resend->pending = tf_ssb_ebt_get_send_clock_pending(ebt);
tf_ssb_connection_schedule_idle(connection, "ebt.clock", _tf_ssb_rpc_ebt_replicate_resend_clock, resend);
}
else
{
tf_ssb_ebt_set_send_clock_pending(ebt, 0);
tf_free(resend);
}
_tf_ssb_rpc_ebt_replicate_send_messages(connection);
}
static void _tf_ssb_rpc_ebt_replicate_resend_clock(tf_ssb_connection_t* connection, bool skip, void* user_data)
{
resend_clock_t* resend = user_data;
if (!skip)
{
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
tf_ssb_ebt_get_send_clock(ebt, resend->request_number, _tf_ssb_rpc_ebt_send_clock_callback, resend);
}
else
{
tf_free(resend);
}
} }
static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data) static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, JSValue args, const uint8_t* message, size_t size, void* user_data)
@ -1065,38 +1207,33 @@ static void _tf_ssb_rpc_ebt_replicate(tf_ssb_connection_t* connection, uint8_t f
JSValue name = JS_GetPropertyStr(context, args, "name"); JSValue name = JS_GetPropertyStr(context, args, "name");
JSValue in_clock = JS_IsUndefined(name) ? args : JS_UNDEFINED; JSValue in_clock = JS_IsUndefined(name) ? args : JS_UNDEFINED;
bool resend_clock = false;
tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
if (!JS_IsUndefined(author)) if (!JS_IsUndefined(author))
{ {
/* Looks like a message. */ /* Looks like a message. */
tf_ssb_connection_adjust_read_backpressure(connection, 1); tf_ssb_connection_adjust_read_backpressure(connection, 1);
tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection); tf_ssb_verify_strip_and_store_message(ssb, args, _tf_ssb_rpc_ebt_replicate_store_callback, connection);
resend_clock = !tf_ssb_is_shutting_down(ssb) && !tf_ssb_connection_is_closing(connection); if (tf_ssb_connection_get_sent_clock(connection))
}
else
{
tf_ssb_ebt_receive_clock(ebt, context, in_clock);
resend_clock = true;
}
if (resend_clock && tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
{
int pending = tf_ssb_ebt_get_send_clock_pending(ebt) + 1;
tf_ssb_ebt_set_send_clock_pending(ebt, pending);
if (pending == 1)
{ {
tf_ssb_connection_set_sent_clock(connection, false);
resend_clock_t* resend = tf_malloc(sizeof(resend_clock_t)); resend_clock_t* resend = tf_malloc(sizeof(resend_clock_t));
*resend = (resend_clock_t) { *resend = (resend_clock_t) {
.connection = connection, .connection = connection,
.request_number = request_number, .request_number = request_number,
.pending = pending,
}; };
tf_ssb_connection_schedule_idle(connection, "ebt.clock", _tf_ssb_rpc_ebt_replicate_resend_clock, resend); tf_ssb_connection_schedule_idle(connection, _tf_ssb_rpc_ebt_replicate_resend_clock, resend);
} }
} }
else
{
/* EBT clock. */
if (!tf_ssb_connection_get_sent_clock(connection))
{
_tf_ssb_rpc_ebt_replicate_send_clock(connection, request_number, in_clock);
tf_ssb_connection_set_sent_clock(connection, true);
}
_tf_ssb_rpc_ebt_replicate_send_messages(connection, in_clock);
}
JS_FreeValue(context, name); JS_FreeValue(context, name);
JS_FreeValue(context, author); JS_FreeValue(context, author);
} }
@ -1263,17 +1400,14 @@ typedef struct _delete_t
static void _tf_ssb_rpc_delete_blobs_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_rpc_delete_blobs_work(tf_ssb_t* ssb, void* user_data)
{ {
delete_t* delete = user_data; delete_t* delete = user_data;
int64_t age = -1; int64_t age = _get_global_setting_int64(ssb, "blob_expire_age_seconds", -1);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_int64(db, "blob_expire_age_seconds", &age);
tf_ssb_release_db_reader(ssb, db);
if (age <= 0) if (age <= 0)
{ {
_tf_ssb_rpc_checkpoint(ssb); _tf_ssb_rpc_checkpoint(ssb);
return; return;
} }
int64_t start_ns = uv_hrtime(); int64_t start_ns = uv_hrtime();
db = tf_ssb_acquire_db_writer(ssb); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement; sqlite3_stmt* statement;
int64_t now = (int64_t)time(NULL) * 1000ULL; int64_t now = (int64_t)time(NULL) * 1000ULL;
int64_t timestamp = now - age * 1000ULL; int64_t timestamp = now - age * 1000ULL;
@ -1335,18 +1469,12 @@ static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms)
static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data)
{ {
delete_t* delete = user_data; delete_t* delete = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb); if (!_get_global_setting_bool(ssb, "delete_stale_feeds", false))
bool delete_stale_feeds = false;
tf_ssb_db_get_global_setting_bool(db, "delete_stale_feeds", &delete_stale_feeds);
if (!delete_stale_feeds)
{ {
tf_ssb_release_db_reader(ssb, db);
return; return;
} }
int64_t start_ns = uv_hrtime(); int64_t start_ns = uv_hrtime();
int64_t replication_hops = 2; int replication_hops = (int)_get_global_setting_int64(ssb, "replication_hops", 2);
tf_ssb_db_get_global_setting_int64(db, "replication_hops", &replication_hops);
tf_ssb_release_db_reader(ssb, db);
const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops); const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops);
JSMallocFunctions funcs = { 0 }; JSMallocFunctions funcs = { 0 };
@ -1365,7 +1493,7 @@ static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data)
JS_FreeValue(context, json); JS_FreeValue(context, json);
JS_FreeValue(context, array); JS_FreeValue(context, array);
db = tf_ssb_acquire_db_writer(ssb); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement; sqlite3_stmt* statement;
if (sqlite3_prepare(db, if (sqlite3_prepare(db,
"DELETE FROM messages WHERE author IN (" "DELETE FROM messages WHERE author IN ("

View File

@ -536,7 +536,7 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
uv_run(&loop, UV_RUN_NOWAIT); uv_run(&loop, UV_RUN_NOWAIT);
tf_ssb_connection_close(tun0, "done"); tf_ssb_connection_close(tun0);
uv_run(&loop, UV_RUN_NOWAIT); uv_run(&loop, UV_RUN_NOWAIT);

View File

@ -17,7 +17,6 @@
#include "util.js.h" #include "util.js.h"
#include "version.h" #include "version.h"
#include "ares.h"
#include "backtrace.h" #include "backtrace.h"
#include "quickjs.h" #include "quickjs.h"
#include "sqlite3.h" #include "sqlite3.h"
@ -702,7 +701,6 @@ static JSValue _tf_task_version(JSContext* context, JSValueConst this_val, int a
JS_SetPropertyStr(context, version, "openssl", JS_NewString(context, OpenSSL_version(OPENSSL_VERSION))); JS_SetPropertyStr(context, version, "openssl", JS_NewString(context, OpenSSL_version(OPENSSL_VERSION)));
#endif #endif
const char* sodium_version_string(); const char* sodium_version_string();
JS_SetPropertyStr(context, version, "c-ares", JS_NewString(context, ares_version(NULL)));
JS_SetPropertyStr(context, version, "libsodium", JS_NewString(context, sodium_version_string())); JS_SetPropertyStr(context, version, "libsodium", JS_NewString(context, sodium_version_string()));
JS_SetPropertyStr(context, version, "zlib", JS_NewString(context, zlibVersion())); JS_SetPropertyStr(context, version, "zlib", JS_NewString(context, zlibVersion()));
tf_trace_end(task->_trace); tf_trace_end(task->_trace);
@ -934,6 +932,21 @@ char* tf_task_get_hitches(tf_task_t* task)
return result; return result;
} }
char* tf_task_get_disconnections(tf_task_t* task)
{
JSContext* context = task->_context;
tf_trace_begin(task->_trace, __func__);
JSValue object = tf_ssb_get_disconnection_debug(task->_ssb, context);
JSValue json = JS_JSONStringify(context, object, JS_NULL, JS_NewInt32(context, 2));
const char* string = JS_ToCString(context, json);
char* result = tf_strdup(string);
JS_FreeCString(context, string);
JS_FreeValue(context, json);
JS_FreeValue(context, object);
tf_trace_end(task->_trace);
return result;
}
static JSValue _tf_task_getFile(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_task_getFile(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
tf_task_t* task = JS_GetContextOpaque(context); tf_task_t* task = JS_GetContextOpaque(context);

View File

@ -310,6 +310,14 @@ void tf_task_remove_child(tf_task_t* task, tf_taskstub_t* child);
*/ */
bool tf_task_send_error_to_parent(tf_task_t* task, JSValue error); bool tf_task_send_error_to_parent(tf_task_t* task, JSValue error);
/**
** Get a report of recent disconnections.
** @param task The task.
** @return A JSON representation of recent disconnections that must be freed
** with tf_free().
*/
char* tf_task_get_disconnections(tf_task_t* task);
/** /**
** Get a report of miscellaneous debug information. ** Get a report of miscellaneous debug information.
** @param task The task. ** @param task The task.

View File

@ -52,8 +52,7 @@ static void _test_nop(const tf_test_options_t* options)
_write_file("out/test.js", "print('hi');"); _write_file("out/test.js", "print('hi');");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -66,8 +65,7 @@ static void _test_exception(const tf_test_options_t* options)
_write_file("out/test.js", "throw new Error('oops');"); _write_file("out/test.js", "throw new Error('oops');");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("result = %d\n", result); tf_printf("result = %d\n", result);
@ -100,8 +98,7 @@ static void _test_sandbox(const tf_test_options_t* options)
"exit(r);\n"); "exit(r);\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -133,8 +130,7 @@ static void _test_child(const tf_test_options_t* options)
"exit(0);\n"); "exit(0);\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -172,8 +168,7 @@ static void _test_promise(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -215,8 +210,7 @@ static void _test_promise_remote_throw(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -260,8 +254,7 @@ static void _test_promise_remote_reject(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -338,8 +331,7 @@ static void _test_this(const tf_test_options_t* options)
"exit(0);\n"); "exit(0);\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -369,8 +361,7 @@ static void _test_await(const tf_test_options_t* options)
"\n"); "\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -400,16 +391,14 @@ static void _test_import(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
assert(WIFEXITED(result)); assert(WIFEXITED(result));
assert(WEXITSTATUS(result) == 0); assert(WEXITSTATUS(result) == 0);
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/bad.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/bad.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
result = system(command); result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -427,8 +416,7 @@ static void _test_exit(const tf_test_options_t* options)
_write_file("out/blah.js", "\n"); _write_file("out/blah.js", "\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -446,8 +434,7 @@ static void _test_icu(const tf_test_options_t* options)
"print(parseInt('3').toLocaleString());\n"); "print(parseInt('3').toLocaleString());\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -495,8 +482,7 @@ static void _test_uint8array(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -537,8 +523,7 @@ static void _test_float(const tf_test_options_t* options)
"print(\"child ready\");\n"); "print(\"child ready\");\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
(void)result; (void)result;
@ -625,8 +610,7 @@ static void _test_socket(const tf_test_options_t* options)
"});\n"); "});\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));
@ -675,8 +659,7 @@ static void _test_b64(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
unlink("out/test_db0.sqlite"); snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path);
snprintf(command, sizeof(command), "%s run --db-path=out/test_db0.sqlite -s out/test.js" TEST_ARGS, options->exe_path);
tf_printf("%s\n", command); tf_printf("%s\n", command);
int result = system(command); int result = system(command);
tf_printf("returned %d\n", WEXITSTATUS(result)); tf_printf("returned %d\n", WEXITSTATUS(result));

View File

@ -339,7 +339,7 @@ static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this
.default_value = _is_mobile() ? JS_NewInt32(context, (int)(1.0f * 365 * 24 * 60 * 60)) : JS_UNDEFINED }, .default_value = _is_mobile() ? JS_NewInt32(context, (int)(1.0f * 365 * 24 * 60 * 60)) : JS_UNDEFINED },
{ .name = "fetch_hosts", .type = "string", .description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty." }, { .name = "fetch_hosts", .type = "string", .description = "Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty." },
{ .name = "http_redirect", .type = "string", .description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")" }, { .name = "http_redirect", .type = "string", .description = "If connecting by HTTP and HTTPS is configured, Location header prefix (ie, \"http://example.com\")" },
{ .name = "index", .type = "string", .description = "Default path.", .default_value = JS_NewString(context, "/~core/ssb/") }, { .name = "index", .type = "string", .description = "Default path.", .default_value = JS_NewString(context, "/~core/apps") },
{ .name = "index_map", .type = "textarea", .description = "Mappings from hostname to redirect path, one per line, as in: \"www.tildefriends.net=/~core/index/\"" }, { .name = "index_map", .type = "textarea", .description = "Mappings from hostname to redirect path, one per line, as in: \"www.tildefriends.net=/~core/index/\"" },
{ .name = "peer_exchange", { .name = "peer_exchange",
.type = "boolean", .type = "boolean",
@ -356,7 +356,7 @@ static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this
.default_value = JS_NewInt32(context, 2) }, .default_value = JS_NewInt32(context, 2) },
{ .name = "delete_stale_feeds", { .name = "delete_stale_feeds",
.type = "boolean", .type = "boolean",
.description = "Periodically delete feeds that aren't visible from local accounts or related follows.", .description = "Periodically delete feeds that visible from local accounts and related follows.",
.default_value = JS_FALSE }, .default_value = JS_FALSE },
}; };

View File

@ -150,19 +150,6 @@ const char* tf_util_function_to_string(void* function);
_a > _b ? _b : _a; \ _a > _b ? _b : _a; \
}) })
/**
** Get the maximum of two values.
** @param a The first value.
** @param b The second value.
** @return The maximum of a and b.
*/
#define tf_max(a, b) \
({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
/** /**
** Get the number of elements in an array. ** Get the number of elements in an array.
** @param a The array. ** @param a The array.

View File

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.27-wip" #define VERSION_NUMBER "0.0.26"
#define VERSION_NAME "This program kills fascists." #define VERSION_NAME "This program kills fascists."

View File

@ -71,7 +71,7 @@ try:
driver = webdriver.Firefox(options = options, service = service) driver = webdriver.Firefox(options = options, service = service)
wait = WebDriverWait(driver, 10) wait = WebDriverWait(driver, 10)
driver.get('http://localhost:8888/~core/apps/') driver.get('http://localhost:8888')
select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',)) select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'adminuser')) select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'adminuser'))
@ -89,7 +89,7 @@ try:
select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))
driver.get('http://localhost:8888/~core/apps/') driver.get('http://localhost:8888')
select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=login'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',)) select(driver, ['tf-auth', 'shadow_root', '#register_label'], ('click',))
select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser')) select(driver, ['tf-auth', 'shadow_root', '#name'], ('send_keys', 'testuser'))
@ -140,7 +140,7 @@ try:
select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#close_error'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '=edit'], ('click',))
driver.get('http://localhost:8888/~core/apps/') driver.get('http://localhost:8888')
select(driver, ['#document', 'frame', '=identity']) select(driver, ['#document', 'frame', '=identity'])
@ -170,7 +170,7 @@ try:
id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1] id1 = select(driver, ['#document', 'frame', 'li']).text.split(' ')[-1]
assert id0 == id1 assert id0 == id1
driver.get('http://localhost:8888/~core/apps/') driver.get('http://localhost:8888')
select(driver, ['#document', 'frame', '=ssb'], ('click',)) select(driver, ['#document', 'frame', '=ssb'], ('click',))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'Hello, world!')) select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#edit'], ('send_keys', 'Hello, world!'))
select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',)) select(driver, ['#document', 'frame', 'tf-app', 'shadow_root', '#tf-tab-news', 'shadow_root', '#tf-compose', 'shadow_root', '#submit'], ('click',))

View File

@ -73,10 +73,8 @@ build_the_thing() {
no-whirlpool no-whirlpool
no-weak-ssl-ciphers no-weak-ssl-ciphers
no-zlib no-zlib
-Oz -Os
-DOPENSSL_SMALL_FOOTPRINT -DOPENSSL_SMALL_FOOTPRINT
-ffunction-sections
-fdata-sections
-flto" -flto"
pwd pwd
echo "./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS" && \ echo "./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS" && \

View File

@ -29,14 +29,14 @@ do
case $build_target in case $build_target in
ios64-xcrun) ios64-xcrun)
TRIBLE="arm64-darwin-ios" TRIBLE="arm64-darwin-ios"
OPTIONS="--static -static -Os -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined -miphoneos-version-min=9.0" OPTIONS="--static -static -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined -miphoneos-version-min=9.0"
DESTDIR="/tmp/$BUILD_DIR/arm64-ios" DESTDIR="/tmp/$BUILD_DIR/arm64-ios"
SSL_TARGET="ios64-xcrun" SSL_TARGET="ios64-xcrun"
CC=clang CC=clang
;; ;;
iossimulator-xcrun) iossimulator-xcrun)
TRIBLE="x86_64-darwin-ios" TRIBLE="x86_64-darwin-ios"
OPTIONS="--static -static -Os -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined" OPTIONS="--static -static -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined"
DESTDIR="/tmp/$BUILD_DIR/x86_64-iossim" DESTDIR="/tmp/$BUILD_DIR/x86_64-iossim"
SSL_TARGET="iossimulator-xcrun" SSL_TARGET="iossimulator-xcrun"
CC=clang CC=clang

View File

@ -1,94 +0,0 @@
#!/bin/bash
BUILD_PLATFORM=$(uname -s)
if [[ -z $BUILD_TARGET ]]; then
BUILD_TARGET=$(uname -m)
WORK_DIR=out/openssl-local
else
WORK_DIR=out/openssl-$BUILD_TARGET
if [[ -z $SSL_TARGET ]]; then
SSL_TARGET=linux-$BUILD_TARGET
fi
fi
rm -rf $WORK_DIR
mkdir -p out/
cp -aRf deps/openssl_src/ $WORK_DIR
echo "Building"
pwd
pushd $WORK_DIR || exit 128
rm -rf $DESTDIR
echo $PATH
export GLOBAL_OPTIONS="
no-apps
no-asm
no-async
no-autoerrinit
no-autoload-config
no-cmp
no-cms
no-comp
no-deprecated
no-dgram
no-docs
no-dsa
no-dso
no-dtls
no-dtls1
no-dtls1-method
no-dynamic-engine
no-ec2m
no-egd
no-engine
no-err
no-filenames
no-gost
no-http
no-idea
no-legacy
no-md2
no-md4
no-module
no-multiblock
no-nextprotoneg
no-ocsp
no-psk
no-shared
no-sock
no-srp
no-ssl
no-ssl3
no-ssl-trace
no-stdio
no-tests
no-thread-pool
no-threads
no-tls1
no-tls1-method
no-trace
no-ui-console
no-uplink
no-whirlpool
no-weak-ssl-ciphers
no-zlib
-Os
-DOPENSSL_SMALL_FOOTPRINT
-Wno-error
-ffunction-sections
-fdata-sections"
pwd
echo "./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS" && \
./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS && \
make -s clean && \
make -s build_generated && \
make -s libcrypto.a libssl.a || exit 128
popd
echo WORK_DIR=$WORK_DIR
rm -rf deps/openssl/$BUILD_PLATFORM/$BUILD_TARGET/
mkdir -p deps/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/include/
mkdir -p deps/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/lib/
cp -R $WORK_DIR/include/* deps/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/include/
cp $WORK_DIR/*.a deps/openssl/$BUILD_PLATFORM/$BUILD_TARGET/usr/local/lib/
echo Success