25 Commits

Author SHA1 Message Date
cc409dc3f7 build: Let's build 0.0.25.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m0s
2024-11-27 12:10:17 -05:00
af6091760c ssb+docs: prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 12:07:00 -05:00
e1d93c003c docs: Update docs from wiki.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m5s
2024-11-27 10:13:16 -05:00
ff9dd2dd03 haiku: Disable a bit of a test that is giving me an SQLITE_PROTOCOL error only on Haiku.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 15:05:23 -05:00
7a306bb3d2 build: Fix a regex warning.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m11s
2024-11-27 14:36:50 -05:00
7ffc148358 build: I wanted to get the binary out of the makefile to appease F-Droid, and one thing lead to another.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 09:28:14 -05:00
50fef2edfa build: Fix on OpenBSD. TIL awk.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m11s
2024-11-27 09:06:02 -05:00
aa40084010 build: Redid this thing in sed to make it work on more platforms.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m16s
2024-11-26 22:55:01 -05:00
740d788c7c storage: Show accounts with the most follows, for help pruning accounts.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m5s
2024-11-26 16:25:15 -05:00
4c2fa2c1b3 storage: Show totals, too.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m26s
2024-11-26 16:05:28 -05:00
4350c7b7a9 storage: Add a little app to show something about feed sizes. 2024-11-26 15:59:02 -05:00
595f14d98d docs: Update some docs links to the gitea wiki and generally refresh the README.md slightly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m38s
2024-11-26 11:42:33 -05:00
2e95d6ea63 docs: Add the Tilde Friends gitea wiki as a git submodule to replace the docs directory. Maybe I will succeed at doing something with it if it is more web-facing.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-26 11:30:57 -05:00
0da6abeb98 ssb: We can trace request names these days.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-26 11:14:30 -05:00
e4e050e8e7 ssb: Fix some message link encoding.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m41s
2024-11-26 08:42:51 -05:00
5bc082b75e build: Prepare a changelog for the next release.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m53s
2024-11-25 21:12:00 -05:00
beedbd7646 build: Attempt to self-document the makefile. 2024-11-25 21:11:36 -05:00
507b069ffe cleanup: prettier. 2024-11-25 20:05:40 -05:00
71444b0427 ssb: Shutdown fixes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m12s
2024-11-25 17:14:16 -05:00
a08bba438e update: sqlite 3.47.1. 2024-11-25 13:16:20 -05:00
df1e6711af ssb: Add a setting to periodically clean up un-followed feeds. #80
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 16m5s
2024-11-25 12:53:28 -05:00
f6d4e934e3 ssb: Adjust the follow/hops policies. Replication defaults to 2 hops, counted in the same way as the docs, and is configurable. #79
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-25 11:20:01 -05:00
d5bd4c6735 test: Use -t=auto to update some screenshots. 2024-11-25 09:53:11 -05:00
eb12ba6ed2 test: Use -t=auto to generate some screenshots, detect -t=auto failure more reliably, exercise setting the initial profile, and fix various bugs that fell out.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-25 09:38:49 -05:00
6e83c08535 ssb: Add an index that helps me calculate feed size about 8x faster. 2024-11-23 17:50:32 -05:00
36 changed files with 680 additions and 462 deletions

3
.gitmodules vendored
View File

@ -26,3 +26,6 @@
[submodule "deps/c-ares"] [submodule "deps/c-ares"]
path = deps/c-ares path = deps/c-ares
url = https://github.com/c-ares/c-ares.git url = https://github.com/c-ares/c-ares.git
[submodule "docs"]
path = docs
url = https://dev.tildefriends.net/cory/tildefriends.wiki.git

View File

@ -3,11 +3,24 @@
MAKEFLAGS += --warn-undefined-variables MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
## == Tilde Friends build. ==
##
## This is a list of all supported build targets.
##
## Consider passing -j$(nproc) or adding it to your $MAKEFLAGS to build in
## parallel (faster).
##
## Useful variables to override:
## CC := Compiler.
## AS := Assembler.
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 30 VERSION_CODE := 30
VERSION_NUMBER := 0.0.25-wip VERSION_NUMBER := 0.0.25
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
@ -747,11 +760,28 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-framework UIKit \ -framework UIKit \
-framework WebKit -framework WebKit
unix: debug release ##
win: windebug winrelease ## Common targets:
all: $(BUILD_TYPES) ##
debug: ## Build a debug executable for the current platform.
release: ## Build a release executable for the current platform.
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
unix: debug release ## Build all UNIX targets.
win: windebug winrelease ## Build all Windows targets.
.PHONY: all win unix .PHONY: all win unix
##
## Windows targets:
##
windebug: ## Build a debug win32 executable.
winrelease: ## Build a release win32 executable.
##
## MacOS targets:
##
macosdebug: ## Build a MacOS debug executable.
macosrelease: ## Build a MacOS release executable.
ALL_APP_OBJS := \ ALL_APP_OBJS := \
$(APP_OBJS) \ $(APP_OBJS) \
$(ARES_OBJS) \ $(ARES_OBJS) \
@ -807,7 +837,18 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \ -e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
$@ $@
# Android support. ##
## Android targets:
##
androiddebug: ## Build a debug 64-bit ARM Android APK.
androidrelease: ## Build a release 64-bit ARM Android APK.
androiddebug-armv7a: ## Build a debug 32-bit ARM Android APK.
androidrelease-armv7a: ## Build a release 32-bit ARM Android APK.
androiddebug-x86: ## Build a debug x86 Android APK.
androidrelease-x86: ## Build a release x86 Android APK.
androiddebug-x86_64: ## Build a debug x86_64 Android APK.
androidrelease-x86_64: ## Build a release x86_64 Android APK.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@echo "[aapt2] $@" @echo "[aapt2] $@"
@ -914,7 +955,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@ @java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android @jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
aab: out/TildeFriends.aab aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab .PHONY: aab
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL) out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
@ -981,20 +1022,32 @@ out/%.zopfli.apk: out/%.apk
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli $(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli @$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
.PHONY: release-apk .PHONY: release-apk
apkgo: out/TildeFriends-arm-debug.apk fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk ## Build Android APK for distribution on F-Droid.
.PHONY: fdroid
apkgo: out/TildeFriends-arm-debug.apk ## Build, install, and run a debug Android APK.
@adb install -r $< @adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity @adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: apkgo .PHONY: apkgo
releaseapkgo: out/TildeFriends-arm-release.apk releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a release Android APK.
@adb install -r $< @adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity @adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo .PHONY: releaseapkgo
# iOS Support apklog: ## Display Android log output.
@adb logcat *:S tildefriends
.PHONY: apklog
##
## iPhoneOS targets:
##
iosdebug: ## Build a debug iPhoneOS executable.
iosrelease: ## Build a release iPhoneOS executable.
out/%.app/Info.plist: src/ios/Info.plist out/%.app/Info.plist: src/ios/Info.plist
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@cp -v $< $@ @cp -v $< $@
@ -1031,39 +1084,23 @@ out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
@cat $< out/data.zip > $@ @cat $< out/data.zip > $@
@chmod +x $@ @chmod +x $@
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends ## Build a debug iOS Simulator .app directory.
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends ## Build a release iOS Simulator .app directory.
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends iosdebug-app: out/tildefriends-iosdebug.app/tildefriends ## Build a debug iOS .app directory.
iosrelease-app: out/tildefriends-iosrelease.app/tildefriends iosrelease-app: out/tildefriends-iosrelease.app/tildefriends ## Build a release iOS .app directory.
iosdebug-ipa: out/tildefriends-debug.ipa iosdebug-ipa: out/tildefriends-debug.ipa ## Build a debug iOS .ipa.
iosrelease-ipa: out/tildefriends-release.ipa iosrelease-ipa: out/tildefriends-release.ipa ## Build a release iOS .ipa.
.PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app .PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app
ios%go: out/tildefriends-ios%.app/tildefriends ios%go: out/tildefriends-ios%.app/tildefriends
ideviceinstaller -i $(realpath $(dir $<)) ideviceinstaller -i $(realpath $(dir $<))
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install, and run an iOS debug build.
xcrun simctl install booted out/tildefriends-iossimdebug.app/ xcrun simctl install booted out/tildefriends-iossimdebug.app/
xcrun simctl launch booted com.unprompted.tildefriends xcrun simctl launch booted com.unprompted.tildefriends
.PHONY: iossimdebuggo .PHONY: iossimdebuggo
apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
fetchdeps:
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONY: fetchdeps
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS): $(ANDROID_DEPS):
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android +@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
@ -1083,6 +1120,10 @@ $(IOS_DEPS):
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS) $(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif endif
##
## Linux package targets:
##
out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@echo "[appimage] $$@" @echo "[appimage] $$@"
@rm -rf out/tildefriends.AppDir @rm -rf out/tildefriends.AppDir
@ -1102,19 +1143,36 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@cd out; ./appimagetool --appimage-extract; cd .. @cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd .. @cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
.PHONY: appimage .PHONY: appimage
flatpak: out/ flatpak: out/ ## Build a flatpak.
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
.PHONY: flatpak .PHONY: flatpak
clean: ##
rm -rf $(BUILD_DIR) ## Targets for release management:
.PHONY: clean ##
tarball: fetchdeps: ## Update various external sources that live in the tree that can't be pulled in as git submodules.
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONY: fetchdeps
shots: ## Copy generated screenshots from `tildefriends test -t=auto` into place in the metadata/ directory.
@echo [shots] $(wildcard out/screenshot*.png)
@cp -f out/screenshot*.png metadata/en-US/images/phoneScreenshots/
.PHONY: shots
tarball: ## Build an all-inclusive source tarball (.tar.xz).
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz @echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf out/tildefriends-$(VERSION_NUMBER) @rm -rf out/tildefriends-$(VERSION_NUMBER)
@mkdir -p out/tildefriends-$(VERSION_NUMBER) @mkdir -p out/tildefriends-$(VERSION_NUMBER)
@ -1139,6 +1197,7 @@ tarball:
tildefriends-$(VERSION_NUMBER) tildefriends-$(VERSION_NUMBER)
.PHONY: tarball .PHONY: tarball
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 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"
@ -1159,24 +1218,53 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@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
.PHONY: dist .PHONY: dist
dist-test: dist dist-test: dist ## Exercise some built distributable files, making sure they work as intended.
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz @tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release @$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
@docker build tildefriends-$(VERSION_NUMBER)/ @docker build tildefriends-$(VERSION_NUMBER)/
@rm -rf tildefriends-$(VERSION_NUMBER) @rm -rf tildefriends-$(VERSION_NUMBER)
.PHONY: dist-test .PHONY: dist-test
format: ##
## Targets for tidying up:
##
format: ## Standardize formatting of C source.
@clang-format -i $(wildcard src/*.c src/*.h src/*.m) @clang-format -i $(wildcard src/*.c src/*.h src/*.m)
.PHONY: format .PHONY: format
prettier: prettier: ## Standardize formatting of JavaScript and Markdown source.
@npm run prettier @npm run prettier
.PHONY: prettier .PHONY: prettier
docs: clean: ## Clean all generated files from the out/ directory.
rm -rf $(BUILD_DIR)
.PHONY: clean
##
## Documentation:
##
help: ## Display this help message.
@awk \
-F: \
-vG=$$(tput setaf 2) \
-vO=$$(tput setaf 3) \
-vB=$$(tput setaf 4) \
-vM=$$(tput setaf 5) \
-vC=$$(tput setaf 6) \
-vR=$$(tput sgr0) ' \
/^## ==.*==$$/ { sub(/^## ?/, ""); printf "%s%s%s\n", C, $$0, R } \
/^##.*:=.*/ { sub(/^## ?/, ""); sub(/:=/, ":"); printf " %s%-20s%s %s%s%s\n", M, $$1, R, O, $$2, R } \
/^##/ { sub(/^## ?/, ""); print $$0 } \
/^[[:alnum:]-]+:.*##/ { \
sub(/:.*##\s?/, ":"); \
printf " %s%-20s%s %s%s%s\n", G, $$1, R, O, $$2, R \
} \
' < $(filter-out %.d,$(MAKEFILE_LIST))
@echo "" # Blank line.
.PHONY: help
.DEFAULT_GOAL := help
docs: ## Build HTML docs.
@doxygen @doxygen
.PHONY: docs .PHONY: docs
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
.PHONY: fdroid

View File

@ -14,10 +14,7 @@ Scuttlebutt, as well as a platform for writing and running web applications.
3. Make creating and sharing web applications accessible to anyone with a 3. Make creating and sharing web applications accessible to anyone with a
browser. browser.
## Building ## Getting the Source
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
Tilde Friends uses git submodules, so either: Tilde Friends uses git submodules, so either:
@ -35,20 +32,35 @@ git submodule update --init --recursive
The `.tar.xz` source releases are all-inclusive. The `.tar.xz` source releases are all-inclusive.
1. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are ## Building
assumed to be available.
2. To build, run `make debug` or `make release`. An executable will be Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible
generated in a subdirectory of `out/`. to build for Android, iOS, and Windows on Linux, if you have the right
3. It's possible to build for Android, iOS, and Windows on Linux, if you have dependencies in the right places.
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`. ### Requirements
4. To build in docker, `docker build .`.
5. `make format` will normalize formatting to the coding standard. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
On MacOS, Xcode's command-line tools are expected to be available.
### Build Commands
Run `make` with no arguments to see available build targets and options. `make
debug` is a good place to start.
To build in docker, `docker build .`.
`make format` and `make prettier` will normalize formatting to the coding
standard.
## Running ## Running
By default, running the built `tildefriends` executable will start a web server By default, running the built `out/debug/tildefriends` executable will start a
at <http://localhost:12345/>. `tildefriends -h` lists further options. web server at <http://localhost:12345/>. It expects to be run with the
repository root as the current working directory. `tildefriends -h` lists
further options.
The first user to create an account and log in will be granted administrative The first user to create an account and log in will be granted administrative
privileges. Further administration can be done at privileges. Further administration can be done at
@ -57,7 +69,7 @@ privileges. Further administration can be done at
## Documentation ## Documentation
Docs are a work in progress: Docs are a work in progress:
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>. <https://dev.tildefriends.net/cory/tildefriends/wiki>.
## License ## License

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🐌", "emoji": "🐌",
"previous": "&Cqu8pxYxC8fBQuUpa3z2kVnX5cqbQ+p5mhqySZwWwb4=.sha256" "previous": "&ksxKqT3Bkp0Z2zV2dQU4ttVZ1k16zdWoJVv6R7m5yAQ=.sha256"
} }

View File

@ -221,7 +221,7 @@ class TfElement extends LitElement {
async load() { async load() {
let whoami = this.whoami; let whoami = this.whoami;
let tags = this.load_recent_tags(); let tags = this.load_recent_tags();
let following = await tfrpc.rpc.following([whoami], 2); let following = await tfrpc.rpc.following([whoami], 3);
let users = {}; let users = {};
let by_count = []; let by_count = [];
for (let [id, v] of Object.entries(following)) { for (let [id, v] of Object.entries(following)) {

View File

@ -408,9 +408,10 @@ ${JSON.stringify(mention, null, 2)}</pre
> >
<tf-user id=${self.message.author} .users=${self.users}></tf-user> <tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px" <span style="padding-right: 8px"
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date( ><a tfarget="_top" href=${'#' + encodeURIComponent(self.message.id)}
self.message.timestamp >%</a
).toLocaleString()}</span >
${new Date(self.message.timestamp).toLocaleString()}</span
> >
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner} ${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()} ${self.render_votes()}
@ -449,7 +450,9 @@ ${JSON.stringify(mention, null, 2)}</pre
class="w3-card-4 w3-theme-d4 w3-border-theme" class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere" style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
> >
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> <a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>${this.message.id}</a
>
(placeholder) (placeholder)
<div>${this.render_votes()}</div> <div>${this.render_votes()}</div>
${(this.message.child_messages || []).map( ${(this.message.child_messages || []).map(
@ -600,7 +603,11 @@ ${JSON.stringify(content, null, 2)}</pre
${is_encrypted} ${is_encrypted}
<span style="flex: 1"></span> <span style="flex: 1"></span>
<span style="padding-right: 8px" <span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a> ><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span ${new Date(this.message.timestamp).toLocaleString()}</span
> >
<span>${raw_button}</span> <span>${raw_button}</span>
@ -643,7 +650,11 @@ ${JSON.stringify(content, null, 2)}</pre
${is_encrypted} ${is_encrypted}
<span style="flex: 1"></span> <span style="flex: 1"></span>
<span style="padding-right: 8px" <span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a> ><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span ${new Date(this.message.timestamp).toLocaleString()}</span
> >
<span>${raw_button}</span> <span>${raw_button}</span>
@ -733,7 +744,11 @@ ${JSON.stringify(content, null, 2)}</pre
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span> <span style="flex: 1"></span>
<span style="padding-right: 8px" <span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a> ><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span ${new Date(this.message.timestamp).toLocaleString()}</span
> >
<span>${raw_button}</span> <span>${raw_button}</span>

View File

@ -233,7 +233,11 @@ class TfProfileElement extends LitElement {
</button>`; </button>`;
} }
edit = html` edit = html`
<button class="w3-button w3-theme-d1" @click=${this.save_edits}> <button
id="save_profile"
class="w3-button w3-theme-d1"
@click=${this.save_edits}
>
Save Profile Save Profile
</button> </button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
@ -242,7 +246,11 @@ class TfProfileElement extends LitElement {
${server_follow} ${server_follow}
`; `;
} else { } else {
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}> edit = html`<button
id="edit_profile"
class="w3-button w3-theme-d1"
@click=${this.edit}
>
Edit Profile Edit Profile
</button>`; </button>`;
} }

View File

@ -217,7 +217,9 @@ class TfTabConnectionsElement extends LitElement {
<ul class="w3-ul w3-border"> <ul class="w3-ul w3-border">
${this.broadcasts ${this.broadcasts
.filter((x) => x.address) .filter((x) => x.address)
.filter((x) => self.connections.map(c => c.id).indexOf(x.pubkey) == -1) .filter(
(x) => self.connections.map((c) => c.id).indexOf(x.pubkey) == -1
)
.map((x) => self.render_broadcast(x))} .map((x) => self.render_broadcast(x))}
</ul> </ul>
<h2>Connections</h2> <h2>Connections</h2>

View File

@ -109,6 +109,7 @@ class TfTabNewsElement extends LitElement {
render() { render() {
let profile = this.hash.startsWith('#@') let profile = this.hash.startsWith('#@')
? html`<tf-profile ? html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)} id=${this.hash.substring(1)}
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}

5
apps/storage.json Normal file
View File

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💾",
"previous": "&mvGTlWKFR5QM/3nb4fJ2WQq0n/gNKvBmhGDkAvb8ki8=.sha256"
}

127
apps/storage/app.js Normal file
View File

@ -0,0 +1,127 @@
async function query(sql, args) {
let rows = [];
await ssb.sqlAsync(sql, args ?? [], function (row) {
rows.push(row);
});
return rows;
}
async function get_biggest() {
return query(`
select author, sum(length(content)) as size from messages group by author order by size desc limit 10;
`);
}
async function get_total() {
return (
await query(`
select sum(length(content)) as size, count(distinct author) as count from messages;
`)
)[0];
}
async function get_names(identities) {
return query(
`
SELECT author, name FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
messages.content ->> 'name' AS name
FROM messages
JOIN json_each(?) AS identities ON identities.value = messages.author
WHERE
json_extract(messages.content, '$.type') = 'about' AND
content ->> 'about' = messages.author AND name IS NOT NULL)
WHERE author_rank = 1
`,
[JSON.stringify(identities)]
);
}
async function get_most_follows() {
return query(`
select author, count(*) as count
from messages
where content ->> 'type' = 'contact' and content ->> 'following' = true
group by author
order by count desc
limit 10;
`);
}
function nice_size(bytes) {
let value = bytes;
let index = 0;
let units = ['B', 'kB', 'MB', 'GB'];
while (value > 1024 && index < units.length - 1) {
value /= 1024;
index++;
}
return `${Math.round(value * 10) / 10} ${units[index]}`;
}
async function main() {
await app.setDocument(
'<p style="color: #fff">Finding the top 10 largest feeds...</p>'
);
let most_follows = await get_most_follows();
let total = await get_total();
let identities = await ssb.getAllIdentities();
let following1 = await ssb.following(identities, 1);
let following2 = await ssb.following(identities, 2);
let biggest = await get_biggest();
let names = await get_names(
[].concat(
biggest.map((x) => x.author),
most_follows.map((x) => x.author)
)
);
names = Object.fromEntries(names.map((x) => [x.author, x.name]));
for (let item of biggest) {
item.name = names[item.author];
item.following =
identities.indexOf(item.author) != -1
? 0
: following1[item.author] !== undefined
? 1
: following2[item.author] !== undefined
? 2
: undefined;
}
for (let item of most_follows) {
item.name = names[item.author];
}
let html = `<body style="color: #000; background-color: #ddd">\n
<h1>Storage Summary</h1>
<h2>Top 10 Accounts by Size</h2>
<ol>`;
for (let item of biggest) {
html += `<li>
<span style="color: #888">${nice_size(item.size)}</span>
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
</li>
\n`;
}
html += `
</ol>
<h2>Top 10 Accounts by Follows</h2>
<ol>`;
for (let item of most_follows) {
html += `<li>
<span style="color: #888">${item.count}</span>
${following2[item.author] ? '✅' : '🚫'}
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
</li>
\n`;
}
html += `
</ol>
<p>Total <span style="color: #888">${nice_size(total.size)}</span> in ${total.count} accounts.</p>
`;
await app.setDocument(html);
}
main().catch(function (e) {
print(e);
});

View File

@ -172,6 +172,7 @@ class TfNavigationElement extends LitElement {
Manage Identities... Manage Identities...
</button> </button>
<button <button
id="edit_profile"
class="w3-bar-item w3-button w3-border" class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile} @click=${self.edit_profile}
> >

17
deps/sqlite/shell.c vendored
View File

@ -460,7 +460,7 @@ char *sqlite3_fgets(char *buf, int sz, FILE *in){
** that into UTF-8. Otherwise, non-ASCII characters all get translated ** that into UTF-8. Otherwise, non-ASCII characters all get translated
** into '?'. ** into '?'.
*/ */
wchar_t *b1 = malloc( sz*sizeof(wchar_t) ); wchar_t *b1 = sqlite3_malloc( sz*sizeof(wchar_t) );
if( b1==0 ) return 0; if( b1==0 ) return 0;
_setmode(_fileno(in), IsConsole(in) ? _O_WTEXT : _O_U8TEXT); _setmode(_fileno(in), IsConsole(in) ? _O_WTEXT : _O_U8TEXT);
if( fgetws(b1, sz/4, in)==0 ){ if( fgetws(b1, sz/4, in)==0 ){
@ -526,7 +526,7 @@ int sqlite3_fputs(const char *z, FILE *out){
** use O_U8TEXT for everything in text mode. ** use O_U8TEXT for everything in text mode.
*/ */
int sz = (int)strlen(z); int sz = (int)strlen(z);
wchar_t *b1 = malloc( (sz+1)*sizeof(wchar_t) ); wchar_t *b1 = sqlite3_malloc( (sz+1)*sizeof(wchar_t) );
if( b1==0 ) return 0; if( b1==0 ) return 0;
sz = MultiByteToWideChar(CP_UTF8, 0, z, sz, b1, sz); sz = MultiByteToWideChar(CP_UTF8, 0, z, sz, b1, sz);
b1[sz] = 0; b1[sz] = 0;
@ -6833,7 +6833,7 @@ static int seriesBestIndex(
continue; continue;
} }
if( pConstraint->iColumn<SERIES_COLUMN_START ){ if( pConstraint->iColumn<SERIES_COLUMN_START ){
if( pConstraint->iColumn==SERIES_COLUMN_VALUE ){ if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){
switch( op ){ switch( op ){
case SQLITE_INDEX_CONSTRAINT_EQ: case SQLITE_INDEX_CONSTRAINT_EQ:
case SQLITE_INDEX_CONSTRAINT_IS: { case SQLITE_INDEX_CONSTRAINT_IS: {
@ -6841,7 +6841,9 @@ static int seriesBestIndex(
idxNum &= ~0x3300; idxNum &= ~0x3300;
aIdx[5] = i; aIdx[5] = i;
aIdx[6] = -1; aIdx[6] = -1;
#ifndef ZERO_ARGUMENT_GENERATE_SERIES
bStartSeen = 1; bStartSeen = 1;
#endif
break; break;
} }
case SQLITE_INDEX_CONSTRAINT_GE: { case SQLITE_INDEX_CONSTRAINT_GE: {
@ -6849,7 +6851,9 @@ static int seriesBestIndex(
idxNum |= 0x0100; idxNum |= 0x0100;
idxNum &= ~0x0200; idxNum &= ~0x0200;
aIdx[5] = i; aIdx[5] = i;
#ifndef ZERO_ARGUMENT_GENERATE_SERIES
bStartSeen = 1; bStartSeen = 1;
#endif
break; break;
} }
case SQLITE_INDEX_CONSTRAINT_GT: { case SQLITE_INDEX_CONSTRAINT_GT: {
@ -6857,7 +6861,9 @@ static int seriesBestIndex(
idxNum |= 0x0200; idxNum |= 0x0200;
idxNum &= ~0x0100; idxNum &= ~0x0100;
aIdx[5] = i; aIdx[5] = i;
#ifndef ZERO_ARGUMENT_GENERATE_SERIES
bStartSeen = 1; bStartSeen = 1;
#endif
break; break;
} }
case SQLITE_INDEX_CONSTRAINT_LE: { case SQLITE_INDEX_CONSTRAINT_LE: {
@ -14169,7 +14175,7 @@ static int idxCreateVtabSchema(sqlite3expert *p, char **pzErrmsg){
}else{ }else{
IdxTable *pTab; IdxTable *pTab;
rc = idxGetTableInfo(p->db, zName, &pTab, pzErrmsg); rc = idxGetTableInfo(p->db, zName, &pTab, pzErrmsg);
if( rc==SQLITE_OK ){ if( rc==SQLITE_OK && ALWAYS(pTab!=0) ){
int i; int i;
char *zInner = 0; char *zInner = 0;
char *zOuter = 0; char *zOuter = 0;
@ -31840,7 +31846,6 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss,
char cWait = (char)qss; /* intentional narrowing loss */ char cWait = (char)qss; /* intentional narrowing loss */
if( cWait==0 ){ if( cWait==0 ){
PlainScan: PlainScan:
assert( cWait==0 );
while( (cin = *zLine++)!=0 ){ while( (cin = *zLine++)!=0 ){
if( IsSpace(cin) ) if( IsSpace(cin) )
continue; continue;
@ -31892,7 +31897,6 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss,
if( *zLine != '/' ) if( *zLine != '/' )
continue; continue;
++zLine; ++zLine;
cWait = 0;
CONTINUE_PROMPT_AWAITC(pst, 0); CONTINUE_PROMPT_AWAITC(pst, 0);
qss = QSS_SETV(qss, 0); qss = QSS_SETV(qss, 0);
goto PlainScan; goto PlainScan;
@ -31904,7 +31908,6 @@ static QuickScanState quickscan(char *zLine, QuickScanState qss,
} }
deliberate_fall_through; deliberate_fall_through;
case ']': case ']':
cWait = 0;
CONTINUE_PROMPT_AWAITC(pst, 0); CONTINUE_PROMPT_AWAITC(pst, 0);
qss = QSS_SETV(qss, 0); qss = QSS_SETV(qss, 0);
goto PlainScan; goto PlainScan;

116
deps/sqlite/sqlite3.c vendored
View File

@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
** This file is an amalgamation of many separate C source files from SQLite ** This file is an amalgamation of many separate C source files from SQLite
** version 3.47.0. By combining all the individual C code files into this ** version 3.47.1. By combining all the individual C code files into this
** single large file, the entire code can be compiled as a single translation ** single large file, the entire code can be compiled as a single translation
** unit. This allows many compilers to do optimizations that would not be ** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements ** possible if the files were compiled separately. Performance improvements
@ -18,7 +18,7 @@
** separate file. This file contains only code for the core SQLite library. ** separate file. This file contains only code for the core SQLite library.
** **
** The content in this amalgamation comes from Fossil check-in ** The content in this amalgamation comes from Fossil check-in
** 03a9703e27c44437c39363d0baf82db4ebc9. ** b95d11e958643b969c47a8e5857f3793b9e6.
*/ */
#define SQLITE_CORE 1 #define SQLITE_CORE 1
#define SQLITE_AMALGAMATION 1 #define SQLITE_AMALGAMATION 1
@ -462,9 +462,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.47.0" #define SQLITE_VERSION "3.47.1"
#define SQLITE_VERSION_NUMBER 3047000 #define SQLITE_VERSION_NUMBER 3047001
#define SQLITE_SOURCE_ID "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e" #define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -968,6 +968,13 @@ SQLITE_API int sqlite3_exec(
** filesystem supports doing multiple write operations atomically when those ** filesystem supports doing multiple write operations atomically when those
** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and ** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and
** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE]. ** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE].
**
** The SQLITE_IOCAP_SUBPAGE_READ property means that it is ok to read
** from the database file in amounts that are not a multiple of the
** page size and that do not begin at a page boundary. Without this
** property, SQLite is careful to only do full-page reads and write
** on aligned pages, with the one exception that it will do a sub-page
** read of the first page to access the database header.
*/ */
#define SQLITE_IOCAP_ATOMIC 0x00000001 #define SQLITE_IOCAP_ATOMIC 0x00000001
#define SQLITE_IOCAP_ATOMIC512 0x00000002 #define SQLITE_IOCAP_ATOMIC512 0x00000002
@ -984,6 +991,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000 #define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000
#define SQLITE_IOCAP_IMMUTABLE 0x00002000 #define SQLITE_IOCAP_IMMUTABLE 0x00002000
#define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000 #define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000
#define SQLITE_IOCAP_SUBPAGE_READ 0x00008000
/* /*
** CAPI3REF: File Locking Levels ** CAPI3REF: File Locking Levels
@ -1130,6 +1138,7 @@ struct sqlite3_file {
** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE] ** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE]
** <li> [SQLITE_IOCAP_IMMUTABLE] ** <li> [SQLITE_IOCAP_IMMUTABLE]
** <li> [SQLITE_IOCAP_BATCH_ATOMIC] ** <li> [SQLITE_IOCAP_BATCH_ATOMIC]
** <li> [SQLITE_IOCAP_SUBPAGE_READ]
** </ul> ** </ul>
** **
** The SQLITE_IOCAP_ATOMIC property means that all writes of ** The SQLITE_IOCAP_ATOMIC property means that all writes of
@ -32298,6 +32307,7 @@ SQLITE_PRIVATE void sqlite3RecordErrorOffsetOfExpr(sqlite3 *db, const Expr *pExp
pExpr = pExpr->pLeft; pExpr = pExpr->pLeft;
} }
if( pExpr==0 ) return; if( pExpr==0 ) return;
if( ExprHasProperty(pExpr, EP_FromDDL) ) return;
db->errByteOffset = pExpr->w.iOfst; db->errByteOffset = pExpr->w.iOfst;
} }
@ -42591,6 +42601,7 @@ static void setDeviceCharacteristics(unixFile *pFd){
if( pFd->ctrlFlags & UNIXFILE_PSOW ){ if( pFd->ctrlFlags & UNIXFILE_PSOW ){
pFd->deviceCharacteristics |= SQLITE_IOCAP_POWERSAFE_OVERWRITE; pFd->deviceCharacteristics |= SQLITE_IOCAP_POWERSAFE_OVERWRITE;
} }
pFd->deviceCharacteristics |= SQLITE_IOCAP_SUBPAGE_READ;
pFd->sectorSize = SQLITE_DEFAULT_SECTOR_SIZE; pFd->sectorSize = SQLITE_DEFAULT_SECTOR_SIZE;
} }
@ -50391,7 +50402,7 @@ static int winSectorSize(sqlite3_file *id){
*/ */
static int winDeviceCharacteristics(sqlite3_file *id){ static int winDeviceCharacteristics(sqlite3_file *id){
winFile *p = (winFile*)id; winFile *p = (winFile*)id;
return SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN | return SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN | SQLITE_IOCAP_SUBPAGE_READ |
((p->ctrlFlags & WINFILE_PSOW)?SQLITE_IOCAP_POWERSAFE_OVERWRITE:0); ((p->ctrlFlags & WINFILE_PSOW)?SQLITE_IOCAP_POWERSAFE_OVERWRITE:0);
} }
@ -51779,7 +51790,7 @@ static int winOpen(
int rc = SQLITE_OK; /* Function Return Code */ int rc = SQLITE_OK; /* Function Return Code */
#if !defined(NDEBUG) || SQLITE_OS_WINCE #if !defined(NDEBUG) || SQLITE_OS_WINCE
int eType = flags&0xFFFFFF00; /* Type of file to open */ int eType = flags&0x0FFF00; /* Type of file to open */
#endif #endif
int isExclusive = (flags & SQLITE_OPEN_EXCLUSIVE); int isExclusive = (flags & SQLITE_OPEN_EXCLUSIVE);
@ -57999,18 +58010,26 @@ static const unsigned char aJournalMagic[] = {
** Return true if page pgno can be read directly from the database file ** Return true if page pgno can be read directly from the database file
** by the b-tree layer. This is the case if: ** by the b-tree layer. This is the case if:
** **
** * the database file is open, ** (1) the database file is open
** * there are no dirty pages in the cache, and ** (2) the VFS for the database is able to do unaligned sub-page reads
** * the desired page is not currently in the wal file. ** (3) there are no dirty pages in the cache, and
** (4) the desired page is not currently in the wal file.
*/ */
SQLITE_PRIVATE int sqlite3PagerDirectReadOk(Pager *pPager, Pgno pgno){ SQLITE_PRIVATE int sqlite3PagerDirectReadOk(Pager *pPager, Pgno pgno){
if( pPager->fd->pMethods==0 ) return 0; assert( pPager!=0 );
if( sqlite3PCacheIsDirty(pPager->pPCache) ) return 0; assert( pPager->fd!=0 );
if( pPager->fd->pMethods==0 ) return 0; /* Case (1) */
assert( pPager->fd->pMethods->xDeviceCharacteristics!=0 );
if( (pPager->fd->pMethods->xDeviceCharacteristics(pPager->fd)
& SQLITE_IOCAP_SUBPAGE_READ)==0 ){
return 0; /* Case (2) */
}
if( sqlite3PCacheIsDirty(pPager->pPCache) ) return 0; /* Failed (3) */
#ifndef SQLITE_OMIT_WAL #ifndef SQLITE_OMIT_WAL
if( pPager->pWal ){ if( pPager->pWal ){
u32 iRead = 0; u32 iRead = 0;
(void)sqlite3WalFindFrame(pPager->pWal, pgno, &iRead); (void)sqlite3WalFindFrame(pPager->pWal, pgno, &iRead);
return iRead==0; return iRead==0; /* Condition (4) */
} }
#endif #endif
return 1; return 1;
@ -158939,6 +158958,7 @@ static Expr *removeUnindexableInClauseTerms(
pNew->pLeft->x.pList = pLhs; pNew->pLeft->x.pList = pLhs;
} }
pSelect->pEList = pRhs; pSelect->pEList = pRhs;
pSelect->selId = ++pParse->nSelect; /* Req'd for SubrtnSig validity */
if( pLhs && pLhs->nExpr==1 ){ if( pLhs && pLhs->nExpr==1 ){
/* Take care here not to generate a TK_VECTOR containing only a /* Take care here not to generate a TK_VECTOR containing only a
** single value. Since the parser never creates such a vector, some ** single value. Since the parser never creates such a vector, some
@ -189798,10 +189818,15 @@ static int fts3PoslistPhraseMerge(
if( *p1==POS_COLUMN ){ if( *p1==POS_COLUMN ){
p1++; p1++;
p1 += fts3GetVarint32(p1, &iCol1); p1 += fts3GetVarint32(p1, &iCol1);
/* iCol1==0 indicates corruption. Column 0 does not have a POS_COLUMN
** entry, so this is actually end-of-doclist. */
if( iCol1==0 ) return 0;
} }
if( *p2==POS_COLUMN ){ if( *p2==POS_COLUMN ){
p2++; p2++;
p2 += fts3GetVarint32(p2, &iCol2); p2 += fts3GetVarint32(p2, &iCol2);
/* As above, iCol2==0 indicates corruption. */
if( iCol2==0 ) return 0;
} }
while( 1 ){ while( 1 ){
@ -192972,7 +192997,7 @@ static int fts3EvalNearTest(Fts3Expr *pExpr, int *pRc){
nTmp += p->pRight->pPhrase->doclist.nList; nTmp += p->pRight->pPhrase->doclist.nList;
} }
nTmp += p->pPhrase->doclist.nList; nTmp += p->pPhrase->doclist.nList;
aTmp = sqlite3_malloc64(nTmp*2); aTmp = sqlite3_malloc64(nTmp*2 + FTS3_VARINT_MAX);
if( !aTmp ){ if( !aTmp ){
*pRc = SQLITE_NOMEM; *pRc = SQLITE_NOMEM;
res = 0; res = 0;
@ -194525,10 +194550,11 @@ static int getNextString(
Fts3PhraseToken *pToken; Fts3PhraseToken *pToken;
p = fts3ReallocOrFree(p, nSpace + ii*sizeof(Fts3PhraseToken)); p = fts3ReallocOrFree(p, nSpace + ii*sizeof(Fts3PhraseToken));
if( !p ) goto no_mem;
zTemp = fts3ReallocOrFree(zTemp, nTemp + nByte); zTemp = fts3ReallocOrFree(zTemp, nTemp + nByte);
if( !zTemp ) goto no_mem; if( !zTemp || !p ){
rc = SQLITE_NOMEM;
goto getnextstring_out;
}
assert( nToken==ii ); assert( nToken==ii );
pToken = &((Fts3Phrase *)(&p[1]))->aToken[ii]; pToken = &((Fts3Phrase *)(&p[1]))->aToken[ii];
@ -194543,9 +194569,6 @@ static int getNextString(
nToken = ii+1; nToken = ii+1;
} }
} }
pModule->xClose(pCursor);
pCursor = 0;
} }
if( rc==SQLITE_DONE ){ if( rc==SQLITE_DONE ){
@ -194553,7 +194576,10 @@ static int getNextString(
char *zBuf = 0; char *zBuf = 0;
p = fts3ReallocOrFree(p, nSpace + nToken*sizeof(Fts3PhraseToken) + nTemp); p = fts3ReallocOrFree(p, nSpace + nToken*sizeof(Fts3PhraseToken) + nTemp);
if( !p ) goto no_mem; if( !p ){
rc = SQLITE_NOMEM;
goto getnextstring_out;
}
memset(p, 0, (char *)&(((Fts3Phrase *)&p[1])->aToken[0])-(char *)p); memset(p, 0, (char *)&(((Fts3Phrase *)&p[1])->aToken[0])-(char *)p);
p->eType = FTSQUERY_PHRASE; p->eType = FTSQUERY_PHRASE;
p->pPhrase = (Fts3Phrase *)&p[1]; p->pPhrase = (Fts3Phrase *)&p[1];
@ -194561,11 +194587,9 @@ static int getNextString(
p->pPhrase->nToken = nToken; p->pPhrase->nToken = nToken;
zBuf = (char *)&p->pPhrase->aToken[nToken]; zBuf = (char *)&p->pPhrase->aToken[nToken];
assert( nTemp==0 || zTemp );
if( zTemp ){ if( zTemp ){
memcpy(zBuf, zTemp, nTemp); memcpy(zBuf, zTemp, nTemp);
sqlite3_free(zTemp);
}else{
assert( nTemp==0 );
} }
for(jj=0; jj<p->pPhrase->nToken; jj++){ for(jj=0; jj<p->pPhrase->nToken; jj++){
@ -194575,17 +194599,17 @@ static int getNextString(
rc = SQLITE_OK; rc = SQLITE_OK;
} }
*ppExpr = p; getnextstring_out:
return rc;
no_mem:
if( pCursor ){ if( pCursor ){
pModule->xClose(pCursor); pModule->xClose(pCursor);
} }
sqlite3_free(zTemp); sqlite3_free(zTemp);
if( rc!=SQLITE_OK ){
sqlite3_free(p); sqlite3_free(p);
*ppExpr = 0; p = 0;
return SQLITE_NOMEM; }
*ppExpr = p;
return rc;
} }
/* /*
@ -232806,7 +232830,27 @@ SQLITE_API int sqlite3session_config(int op, void *pArg){
/************** End of sqlite3session.c **************************************/ /************** End of sqlite3session.c **************************************/
/************** Begin file fts5.c ********************************************/ /************** Begin file fts5.c ********************************************/
/*
** This, the "fts5.c" source file, is a composite file that is itself
** assembled from the following files:
**
** fts5.h
** fts5Int.h
** fts5parse.h <--- Generated from fts5parse.y by Lemon
** fts5parse.c <--- Generated from fts5parse.y by Lemon
** fts5_aux.c
** fts5_buffer.c
** fts5_config.c
** fts5_expr.c
** fts5_hash.c
** fts5_index.c
** fts5_main.c
** fts5_storage.c
** fts5_tokenize.c
** fts5_unicode2.c
** fts5_varint.c
** fts5_vocab.c
*/
#if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS5) #if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS5)
#if !defined(NDEBUG) && !defined(SQLITE_DEBUG) #if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
@ -232816,6 +232860,12 @@ SQLITE_API int sqlite3session_config(int op, void *pArg){
# undef NDEBUG # undef NDEBUG
#endif #endif
#ifdef HAVE_STDINT_H
/* #include <stdint.h> */
#endif
#ifdef HAVE_INTTYPES_H
/* #include <inttypes.h> */
#endif
/* /*
** 2014 May 31 ** 2014 May 31
** **
@ -254888,7 +254938,7 @@ static void fts5SourceIdFunc(
){ ){
assert( nArg==0 ); assert( nArg==0 );
UNUSED_PARAM2(nArg, apUnused); UNUSED_PARAM2(nArg, apUnused);
sqlite3_result_text(pCtx, "fts5: 2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e", -1, SQLITE_TRANSIENT); sqlite3_result_text(pCtx, "fts5: 2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e", -1, SQLITE_TRANSIENT);
} }
/* /*
@ -260079,7 +260129,7 @@ static int sqlite3Fts5VocabInit(Fts5Global *pGlobal, sqlite3 *db){
} }
/* Here ends the fts5.c composite file. */
#endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS5) */ #endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS5) */
/************** End of fts5.c ************************************************/ /************** End of fts5.c ************************************************/

15
deps/sqlite/sqlite3.h vendored
View File

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.47.0" #define SQLITE_VERSION "3.47.1"
#define SQLITE_VERSION_NUMBER 3047000 #define SQLITE_VERSION_NUMBER 3047001
#define SQLITE_SOURCE_ID "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e" #define SQLITE_SOURCE_ID "2024-11-25 12:07:48 b95d11e958643b969c47a8e5857f3793b9e69700b8f1469371386369a26e577e"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -652,6 +652,13 @@ SQLITE_API int sqlite3_exec(
** filesystem supports doing multiple write operations atomically when those ** filesystem supports doing multiple write operations atomically when those
** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and ** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and
** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE]. ** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE].
**
** The SQLITE_IOCAP_SUBPAGE_READ property means that it is ok to read
** from the database file in amounts that are not a multiple of the
** page size and that do not begin at a page boundary. Without this
** property, SQLite is careful to only do full-page reads and write
** on aligned pages, with the one exception that it will do a sub-page
** read of the first page to access the database header.
*/ */
#define SQLITE_IOCAP_ATOMIC 0x00000001 #define SQLITE_IOCAP_ATOMIC 0x00000001
#define SQLITE_IOCAP_ATOMIC512 0x00000002 #define SQLITE_IOCAP_ATOMIC512 0x00000002
@ -668,6 +675,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000 #define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000
#define SQLITE_IOCAP_IMMUTABLE 0x00002000 #define SQLITE_IOCAP_IMMUTABLE 0x00002000
#define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000 #define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000
#define SQLITE_IOCAP_SUBPAGE_READ 0x00008000
/* /*
** CAPI3REF: File Locking Levels ** CAPI3REF: File Locking Levels
@ -814,6 +822,7 @@ struct sqlite3_file {
** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE] ** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE]
** <li> [SQLITE_IOCAP_IMMUTABLE] ** <li> [SQLITE_IOCAP_IMMUTABLE]
** <li> [SQLITE_IOCAP_BATCH_ATOMIC] ** <li> [SQLITE_IOCAP_BATCH_ATOMIC]
** <li> [SQLITE_IOCAP_SUBPAGE_READ]
** </ul> ** </ul>
** **
** The SQLITE_IOCAP_ATOMIC property means that all writes of ** The SQLITE_IOCAP_ATOMIC property means that all writes of

1
docs Submodule

Submodule docs added at a40758cc4b

View File

@ -1,63 +0,0 @@
# Tilde Friends Cheat Sheet
Making apps for the impatient tilde friend.
## Prerequisites
- either run your own instance or use [tildefriends.net](https://www.tildefriends.net/)
- register and login
- [optional] use the `ssb` app to create yourself an SSB identity
## Development Process
1. hit the `edit` link from any app or new app URL
2. make sure the path in the text box is under your username: `/~username/app/`
3. write server-side code in `app.js`
4. click the `save` button or press the save hotkey (Alt+S or _[browser-specific modifiers]_+S)
5. see the app reload on the right side
## Output
- **`app.setDocument(html)`** - send HTML to the browser
- **`print(...)`** - send values to the browser's developer console
## Persistence
- **`app.localStorageGet(key)`** -> **`value`**
- **`app.localStorageSet(key, value)`**
- **`database()`**, **`shared_database(key)`**, **`my_shared_database(package, key)`**
- **`db.get(key)`** -> **`value`**
- **`db.set(key, value)`**
- **`db.exchange(key, expected, value)`** -> **`exchanged`**
- **`db.remove(key)`**
- **`db.getAll()`** -> **`[key1, ...]`**
- **`db.getLike(pattern)`** -> **`{key1: value1, ...}`**
## SSB
- **`ssb.createIdentity()`** -> **`id`**
- **`ssb.getIdentities()`** -> **`[id1, ...]`**
- **`ssb.appendMessageWithIdentity(id, content)`** -> **`message_id`**
- **`ssb.blobStore(blob)`** -> **`blob_id`**
- **`ssb.blobGet(id)`** -> **`blob`**
- **`ssb.sqlAsync(query, args, row_callback)`**
## TF-RPC
Stock helper code for calling functions across the web server and browser boundary.
- on the server: `import * as tfrpc from '/tfrpc.js';`
- in the browser: `import * as tfrpc from '/static/tfrpc.js';`
- either direction:
- register a function: `tfrpc.register(function my_function() {});`
- call a remote function: `let promise = tfrpc.rpc.my_function();`
## Share
- give out web links: [https://www.tildefriends.net/~cory/screwble/](https://www.tildefriends.net/~cory/screwble/)
- use the `Attach App` button when composing a post in [the SSB app](https://www.tildefriends.net/~core/ssb/)
## More Docs
- [api reference](https://www.tildefriends.net/~cory/api/)
- [source code](https://dev.tildefriends.net/cory/tildefriends/releases)

View File

@ -1,166 +0,0 @@
# Tilde Friends Developer's Guide
A Tilde Friends application starts with code that runs on a Tilde Friends server, possibly far away from where you wrote it, in a little JavaScript environment, in its own restricted process, with the only access to the outside world being the ability to send messages to the server. This document gives some recipes showing how that can be used to build a functional user-facing application in light of the unique constraints present.
## Example 1: Hello, world!
Of course we must start with a classic.
### app.js
```
app.setDocument('<h1 style="color: #fff">Hello, world!</h1>');
```
### Output
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Hello, world!&lt;/h1&gt;"></iframe>
### Explanation
At a glance, this might seem mundane, but for it to work:
- the server starts a real process for your app and loads your code into it
- your code runs
- `app.setDocument()` sends a message back to the server
- the server interprets the message and redirects it to the browser
- `core/client.js` in the browser receives the message and puts your HTML into an iframe
- your HTML is presented by the browser in an iframe sandbox
But you don't have to think about all that. Call a function, and you see the result.
## Example 2: Hit Counter
Let's take advantage of code running on the server and create a little hit counter using a key value store shared between all visitors.
### app.js
```
async function main() {
let db = await shared_database('visitors');
let count = parseInt((await db.get('visitors')) ?? '0') + 1;
await db.set('visitors', count.toString());
await app.setDocument(`
<h1 style="color: #fff">Welcome, visitor #${count}!</h1>
`);
}
main();
```
### Output
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Welcome, visitor #1!&lt;/h1&gt;"></iframe>
### Explanation
Just as pure browser apps have access to `localStorage`, Tilde Friends apps have access to key-value storage on the server.
The interface is a bit clunky and will likely change someday, but this example gets a database object, from which you can get and set string values by key. There are various on `shared_database` that let you store data that is private to the user or shared by different criteria.
Also, even though any browser-side code is sandboxed, it is allowed to access browser local storage by going through Tilde Friends API, because sometimes that is useful.
## Example 3: Files
Suppose you don't want to create your entire app in a single server-side file as we've done with the previous examples. There are some tools to allow you to begin to organize.
### app.js
```
async function main() {
let html = utf8Decode(await getFile('index.html'));
app.setDocument(html);
}
main();
```
### index.html
```
<html>
<head>
<script type="module" src="script.js"></script>
</head>
<body style="color: #fff">
<h1>File Test</h1>
</body>
</html>
```
### script.js
```
window.addEventListener('load', function() {
document.body.appendChild(document.createTextNode('Hello, world');
});
```
### Output
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>File Test</h1>Hello, world!&lt;/body&gt;"></iframe>
### Explanation
On the server, `utf8Decode(await getFile(fileName))` lets you load a file from your app. In the browser, your app files are made available by HTTP, so you can `<script src="my_script.js"></script>` and such to access them.
## Example 4: Remote Procedure Call
While making calls between the client and the server, it is possible to pass functions across that boundary. `tfrpc.js` is a tiny script which builds on that feature to try to hide some of the complexities.
### app.js
```
import * as tf from '/tfrpc.js';
function sum() {
let s = 0
for (let x of arguments) {
s += x;
}
return s;
}
tf.register(sum);
async function main() {
app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
```
### index.html
```
<html>
<body>
<h1 id='result'>Calculating...</h1>
</body>
<script type="module" src="script.js"></script>
</html>
```
### script.js
```
import * as tf from '/static/tfrpc.js';
window.addEventListener('load', async function() {
document.getElementById('result').innerText = await tf.rpc.sum(1, 2, 3);
});
```
### Output
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>6</h1>&lt;/body&gt;"></iframe>
### Explanation
Here the browser makes an asynchronous call to the server to do some basic math and update its DOM with the result.
With your favorite Vue/Lit/React/... library on the client-side and your favorite Tilde Friends API calls registered with tfrpc, it becomes pretty easy to start extracting interesting information from, say, SQL queries over Secure Scuttlebutt data, and generating complicated, dynamic user interface. These are the building blocks I used to make the current Tilde Friends SSB client interface.
## Conclusion
Tilde Friends is currently a pile of all the parts that I thought I needed to build interesting web applications, tied together by code that tries to walk the fine line between being secure enough to let us safely run code on the same device and being usable enough that you can open a tab in your browser and start building just by typing code.
I don't claim it thoroughly accomplishes either yet, but I believe it is at a stage where it is showing how promising this approach can be, and I am excited for you to take it for a spin and share.

View File

@ -1,19 +0,0 @@
# Release Checklist
- make sure ci is passing
- run the tests
- format + prettier
- update metadata/en-US/changelogs
- git tag
- push
- make dist
- make a release on gitea
- upload the artifacts
- nix
- comment out the hash in default.nix
- update the version
- run `nix-build`
- update the hash
- bump the versions in GNUmakefile for the next release
- make
- commit

View File

@ -1,64 +0,0 @@
# Tilde Friends Vision
Tilde Friends is a tool for making and sharing.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, and an environment for creating and running web applications.
## Why
This is a thing that I wanted to exist and wanted to work on. No other reason.
There is not a business model. I believe it is interesting and unique.
## Goals
1. Make it **easy and fun** to run all sorts of web applications.
2. Provide **security** that is easy to understand and protects your data.
3. Make **creating and sharing** web applications accessible to anyone with a
browser.
## Ways to Use Tilde Friends
1. **Social Network User**: This is a social network first. You are just here,
because your friends are. Or you like how we limit your message length or
short videos or whatever the trend is. If you are ambitious, you click links
and see interactive experiences (apps) that you wouldn't see elsewhere.
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
lists, wiki pages, ..., and you interact with them as though they were
cloud-hosted by a megacorporation. They just work, and you don't think twice.
3. **Group leader**: You host or use a small public instance, installing apps for
a group of friends to use as web visitors.
4. **Developer**: You like to write code and make or improve apps for fun or to
solve problems. When you encounter a Tilde Friends app on a strange server,
you know you can trivially modify it or download it to your own instance.
## Future Goals / Endgame
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
the web interface publicly, but you can sync, install and edit apps, and
otherwise get the full experience from a tiny touch screen.
2. The universal application runtime. The web browser is the universal
platform, but even for the simplest application that you might want to host
for your friends, cloud hosting, containers, and complicated dependencies might
all enter the mix. Tilde Friends, though it is yet another thing to host,
includes everything you need out of the box to run a vast variety of interesting
apps.
Tilde Friends will be built out, gradually providing safe access to host
resources and client resources the same way web browsers extended access to
resources like GPU, persistent storage, cameras, ... over the years.
Not much effort has been put forward yet to having a robust, long-lasting API,
but since the client side longevity is already handled by web browsers, it
seems possible that the server-side API can be managed in a similar way.
3. An awesome development environment. Right now it runs JavaScript from the
first embeddable text editor I could poorly configure enough to edit code,
but it could incorporate a debugger, source control integration a la ssb-git,
merge tools, and transpiling from all sorts of different languages.

View File

@ -0,0 +1,14 @@
* Improve, test, and fix some first time flow issues.
* Show connect failure reasons.
* Actually use CommonMark JS in "safe" mode(!).
* Move app blob handling from JS to C.
* Fixed "publish" command-line interface and used it to add a script that posts development activity updates.
* Minor style improvements.
* Make it possible to see the little graphs on mobile.
* Shutdown fixes.
* Updated dependencies:
* CodeMirror
* c-ares 1.34.3
* libbacktrace
* speedscope 1.21.0
* sqlite 3.47.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -2,7 +2,7 @@
<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="30" android:versionCode="30"
android:versionName="0.0.25-wip"> android:versionName="0.0.25">
<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

@ -220,6 +220,7 @@ typedef struct _tf_ssb_t
bool verbose; bool verbose;
bool store_debug_messages; bool store_debug_messages;
bool shutting_down; bool shutting_down;
bool shutting_down_deferred;
int messages_stored; int messages_stored;
int blobs_stored; int blobs_stored;
@ -1673,7 +1674,7 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
if (callback) if (callback)
{ {
char buffer[64]; char buffer[64];
snprintf(buffer, sizeof(buffer), "request %d", request_number); snprintf(buffer, sizeof(buffer), "request %s:%d", request_name, request_number);
tf_trace_begin(connection->ssb->trace, buffer); tf_trace_begin(connection->ssb->trace, buffer);
PRE_CALLBACK(connection->ssb, callback); PRE_CALLBACK(connection->ssb, callback);
callback(connection, flags, request_number, val, message, size, user_data); callback(connection, flags, request_number, val, message, size, user_data);
@ -1736,7 +1737,7 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
if (callback) if (callback)
{ {
char buffer[64]; char buffer[64];
snprintf(buffer, sizeof(buffer), "request %d", request_number); snprintf(buffer, sizeof(buffer), "request %s:%d", request_name, request_number);
tf_trace_begin(connection->ssb->trace, buffer); tf_trace_begin(connection->ssb->trace, buffer);
PRE_CALLBACK(connection->ssb, callback); PRE_CALLBACK(connection->ssb, callback);
callback(connection, flags, request_number, JS_UNDEFINED, message, size, user_data); callback(connection, flags, request_number, JS_UNDEFINED, message, size, user_data);
@ -2009,7 +2010,7 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
connection->debug_messages[i] = NULL; connection->debug_messages[i] = NULL;
} }
if (--connection->ssb->connection_ref_count == 0 && connection->ssb->shutting_down) if (--connection->ssb->connection_ref_count == 0 && connection->ssb->shutting_down_deferred)
{ {
tf_ssb_destroy(connection->ssb); tf_ssb_destroy(connection->ssb);
} }
@ -2516,12 +2517,6 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_printf("tf_ssb_destroy\n"); tf_printf("tf_ssb_destroy\n");
ssb->shutting_down = true; ssb->shutting_down = true;
if (ssb->connections_tracker)
{
tf_ssb_connections_destroy(ssb->connections_tracker);
ssb->connections_tracker = NULL;
}
if (ssb->broadcast_listener.data && !uv_is_closing((uv_handle_t*)&ssb->broadcast_listener)) if (ssb->broadcast_listener.data && !uv_is_closing((uv_handle_t*)&ssb->broadcast_listener))
{ {
uv_close((uv_handle_t*)&ssb->broadcast_listener, _tf_ssb_on_handle_close); uv_close((uv_handle_t*)&ssb->broadcast_listener, _tf_ssb_on_handle_close);
@ -2637,10 +2632,20 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
tf_ssb_connection_close(connection); tf_ssb_connection_close(connection);
connection = next; connection = next;
} }
uv_run(ssb->loop, UV_RUN_NOWAIT);
tf_printf("Closed.\n"); tf_printf("Closed.\n");
if (ssb->connections_tracker)
{
tf_ssb_connections_destroy(ssb->connections_tracker);
ssb->connections_tracker = NULL;
}
uv_run(ssb->loop, UV_RUN_NOWAIT);
if (ssb->loop == &ssb->own_loop) if (ssb->loop == &ssb->own_loop)
{ {
tf_printf("uv_loop_close\n");
int r = uv_loop_close(ssb->loop); int r = uv_loop_close(ssb->loop);
if (r != 0) if (r != 0)
{ {
@ -2695,6 +2700,7 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
ssb->room_name = NULL; ssb->room_name = NULL;
} }
ssb->shutting_down_deferred = true;
if (ssb->connection_ref_count == 0) if (ssb->connection_ref_count == 0)
{ {
uv_mutex_destroy(&ssb->db_readers_lock); uv_mutex_destroy(&ssb->db_readers_lock);

View File

@ -112,6 +112,10 @@ static void _tf_ssb_connections_get_next_after_work(tf_ssb_t* ssb, int status, v
static void _tf_ssb_connections_timer(uv_timer_t* timer) static void _tf_ssb_connections_timer(uv_timer_t* timer)
{ {
tf_ssb_connections_t* connections = timer->data; tf_ssb_connections_t* connections = timer->data;
if (tf_ssb_is_shutting_down(connections->ssb))
{
return;
}
tf_ssb_connection_t* active[4]; tf_ssb_connection_t* active[4];
int count = tf_ssb_get_connections(connections->ssb, active, tf_countof(active)); int count = tf_ssb_get_connections(connections->ssb, active, tf_countof(active));
if (count < tf_countof(active)) if (count < tf_countof(active))

View File

@ -134,6 +134,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)"); _tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)"); _tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)"); _tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_size_by_author_index ON messages (author, length(content))");
_tf_ssb_db_exec(db, _tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs (" "CREATE TABLE IF NOT EXISTS blobs ("
" id TEXT PRIMARY KEY," " id TEXT PRIMARY KEY,"

View File

@ -642,7 +642,7 @@ static void _tf_ssb_getActiveIdentity_visit(const char* identity, void* user_dat
active_identity_work_t* request = user_data; active_identity_work_t* request = user_data;
if (!*request->identity) if (!*request->identity)
{ {
snprintf(request->identity, sizeof(request->identity), "%s", identity); snprintf(request->identity, sizeof(request->identity), "@%s", identity);
} }
} }
@ -657,6 +657,11 @@ static void _tf_ssb_getActiveIdentity_work(tf_ssb_t* ssb, void* user_data)
{ {
tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request); tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getActiveIdentity_visit, request);
} }
if (!*request->identity && tf_ssb_db_user_has_permission(ssb, request->name, "administration"))
{
tf_ssb_whoami(ssb, request->identity, sizeof(request->identity));
}
} }
static void _tf_ssb_getActiveIdentity_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_getActiveIdentity_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -2336,7 +2341,7 @@ typedef struct _following_t
static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_following_work(tf_ssb_t* ssb, void* user_data)
{ {
following_t* following = user_data; following_t* following = user_data;
following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth); following->out_following = tf_ssb_db_following_deep(ssb, following->ids, following->ids_count, following->depth - 1);
} }
static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_following_after_work(tf_ssb_t* ssb, int status, void* user_data)

View File

@ -18,6 +18,7 @@ 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);
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 int64_t _get_global_setting_int64(tf_ssb_t* ssb, const char* name, int64_t default_value) static int64_t _get_global_setting_int64(tf_ssb_t* ssb, const char* name, int64_t default_value)
{ {
@ -43,6 +44,30 @@ static int64_t _get_global_setting_int64(tf_ssb_t* ssb, const char* name, int64_
return result; 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)
{ {
@ -978,8 +1003,10 @@ static void _tf_ssb_rpc_ebt_replicate_send_clock_work(tf_ssb_connection_t* conne
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection); tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
JSValue full_clock = JS_NewObject(context); JSValue full_clock = JS_NewObject(context);
int64_t depth = _get_global_setting_int64(ssb, "replication_hops", -1);
/* Ask for every identity we know is being followed from local accounts. */ /* Ask for every identity we know is being followed from local accounts. */
const char** visible = tf_ssb_db_get_all_visible_identities(ssb, 2); const char** visible = tf_ssb_db_get_all_visible_identities(ssb, depth - 1);
for (int i = 0; visible[i]; i++) for (int i = 0; visible[i]; i++)
{ {
int64_t sequence = 0; int64_t sequence = 0;
@ -1339,8 +1366,15 @@ static void _tf_ssb_rpc_checkpoint(tf_ssb_t* ssb)
tf_ssb_release_db_writer(ssb, db); tf_ssb_release_db_writer(ssb, db);
} }
typedef struct _delete_t
{
int deleted;
int64_t duration_ms;
} 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;
int64_t age = _get_global_setting_int64(ssb, "blob_expire_age_seconds", -1); int64_t age = _get_global_setting_int64(ssb, "blob_expire_age_seconds", -1);
if (age <= 0) if (age <= 0)
{ {
@ -1382,30 +1416,122 @@ static void _tf_ssb_rpc_delete_blobs_work(tf_ssb_t* ssb, void* user_data)
tf_printf("prepare failed: %s\n", sqlite3_errmsg(db)); tf_printf("prepare failed: %s\n", sqlite3_errmsg(db));
} }
tf_ssb_release_db_writer(ssb, db); tf_ssb_release_db_writer(ssb, db);
int64_t duration_ms = (uv_hrtime() - start_ns) / 1000000LL; delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL;
tf_printf("Deleted %d blobs in %d ms.\n", deleted, (int)duration_ms); tf_printf("Deleted %d blobs in %d ms.\n", deleted, (int)delete->duration_ms);
_tf_ssb_rpc_checkpoint(ssb); _tf_ssb_rpc_checkpoint(ssb);
_tf_ssb_rpc_start_delete_blobs(ssb, deleted ? (int)duration_ms : (15 * 60 * 1000));
} }
static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data)
{ {
delete_t* delete = user_data;
_tf_ssb_rpc_start_delete_blobs(ssb, delete->deleted ? (int)delete->duration_ms : (15 * 60 * 1000));
tf_free(delete);
} }
static void _tf_ssb_rpc_start_delete_callback(tf_ssb_t* ssb, void* user_data) static void _tf_ssb_rpc_start_delete_blobs_callback(tf_ssb_t* ssb, void* user_data)
{ {
tf_ssb_run_work(ssb, _tf_ssb_rpc_delete_blobs_work, _tf_ssb_rpc_delete_blobs_after_work, NULL); delete_t* delete = tf_malloc(sizeof(delete_t));
*delete = (delete_t) { 0 };
tf_ssb_run_work(ssb, _tf_ssb_rpc_delete_blobs_work, _tf_ssb_rpc_delete_blobs_after_work, delete);
} }
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)
{ {
tf_printf("will delete more blobs in %d ms\n", delay_ms); tf_printf("will delete more blobs in %d ms\n", delay_ms);
tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_callback, NULL); tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_blobs_callback, NULL);
}
static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data)
{
delete_t* delete = user_data;
if (!_get_global_setting_bool(ssb, "delete_stale_feeds", false))
{
return;
}
int64_t start_ns = uv_hrtime();
int replication_hops = (int)_get_global_setting_int64(ssb, "replication_hops", 2);
const char** identities = tf_ssb_db_get_all_visible_identities(ssb, replication_hops);
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue array = JS_NewArray(context);
for (int i = 0; identities[i]; i++)
{
JS_SetPropertyUint32(context, array, i, JS_NewString(context, identities[i]));
}
tf_free(identities);
JSValue json = JS_JSONStringify(context, array, JS_NULL, JS_NULL);
const char* arg = JS_ToCString(context, json);
JS_FreeValue(context, json);
JS_FreeValue(context, array);
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement;
if (sqlite3_prepare(db,
"DELETE FROM messages WHERE author IN ("
" SELECT author FROM messages WHERE author NOT IN (SELECT value FROM json_each(?)) GROUP BY author LIMIT 1"
") RETURNING author",
-1, &statement, NULL) == SQLITE_OK)
{
int status = SQLITE_OK;
bool printed = false;
if (sqlite3_bind_text(statement, 1, arg, -1, NULL) == SQLITE_OK)
{
while ((status = sqlite3_step(statement)) == SQLITE_ROW)
{
if (!printed)
{
tf_printf("deleting %s\n", sqlite3_column_text(statement, 0));
printed = true;
delete->deleted++;
}
}
if (status != SQLITE_DONE)
{
tf_printf("deleting feeds: %s\n", sqlite3_errmsg(db));
}
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
JS_FreeCString(context, arg);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL;
tf_printf("Deleted %d feeds in %d ms.\n", delete->deleted, (int)delete->duration_ms);
_tf_ssb_rpc_checkpoint(ssb);
}
static void _tf_ssb_rpc_delete_feeds_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_t* delete = user_data;
_tf_ssb_rpc_start_delete_feeds(ssb, delete->deleted ? (int)delete->duration_ms : (15 * 60 * 1000));
tf_free(delete);
}
static void _tf_ssb_rpc_start_delete_feeds_callback(tf_ssb_t* ssb, void* user_data)
{
delete_t* delete = tf_malloc(sizeof(delete_t));
*delete = (delete_t) { 0 };
tf_ssb_run_work(ssb, _tf_ssb_rpc_delete_feeds_work, _tf_ssb_rpc_delete_feeds_after_work, delete);
}
static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms)
{
tf_printf("will delete more feeds in %d ms\n", delay_ms);
tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_feeds_callback, NULL);
} }
void tf_ssb_rpc_start_periodic(tf_ssb_t* ssb) void tf_ssb_rpc_start_periodic(tf_ssb_t* ssb)
{ {
_tf_ssb_rpc_start_delete_blobs(ssb, 30 * 1000); _tf_ssb_rpc_start_delete_blobs(ssb, 30 * 1000);
_tf_ssb_rpc_start_delete_feeds(ssb, 25 * 1000);
} }
typedef struct _peers_exchange_t typedef struct _peers_exchange_t

View File

@ -329,7 +329,7 @@ static void _taskstub_on_process_exit(uv_process_t* process, int64_t status, int
if (!JS_IsUndefined(stub->_on_exit)) if (!JS_IsUndefined(stub->_on_exit))
{ {
JSValue ref = JS_DupValue(context, stub->_on_exit); JSValue ref = JS_DupValue(context, stub->_on_exit);
JSValue argv[] = { JS_NewInt32(context, status), JS_NewInt32(context, terminationSignal) }; JSValue argv[] = { JS_NewInt64(context, status), JS_NewInt32(context, terminationSignal) };
JSValue result = JS_Call(context, stub->_on_exit, JS_NULL, 2, argv); JSValue result = JS_Call(context, stub->_on_exit, JS_NULL, 2, argv);
tf_util_report_error(context, result); tf_util_report_error(context, result);
JS_FreeValue(context, result); JS_FreeValue(context, result);

View File

@ -10,6 +10,7 @@
#include "util.js.h" #include "util.js.h"
#include <assert.h> #include <assert.h>
#include <inttypes.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@ -873,9 +874,11 @@ static void _test_httpd(const tf_test_options_t* options)
uv_sleep(1000); uv_sleep(1000);
} }
#if !defined(__HAIKU__)
tf_ssb_t* ssb = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL); tf_ssb_t* ssb = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
const char* app_id = tf_ssb_db_get_property(ssb, "core", "path:test"); const char* app_id = tf_ssb_db_get_property(ssb, "core", "path:test");
tf_ssb_destroy(ssb); tf_ssb_destroy(ssb);
#endif
_http_check_status_code("http://localhost:8080/404", 404); _http_check_status_code("http://localhost:8080/404", 404);
_http_check_status_code("http://localhost:8080/", 303); _http_check_status_code("http://localhost:8080/", 303);
@ -888,6 +891,7 @@ static void _test_httpd(const tf_test_options_t* options)
_http_check_status_code("http://localhost:8080/~core/test/nonexistent.txt", 404); _http_check_status_code("http://localhost:8080/~core/test/nonexistent.txt", 404);
_http_check_body_contains("http://localhost:8080/&MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=.sha256/view", "Hello, world!"); _http_check_body_contains("http://localhost:8080/&MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=.sha256/view", "Hello, world!");
#if !defined(__HAIKU__)
char url[1024]; char url[1024];
snprintf(url, sizeof(url), "http://localhost:8080/%s/", app_id); snprintf(url, sizeof(url), "http://localhost:8080/%s/", app_id);
_http_check_body_contains(url, "<title>Tilde Friends</title>"); _http_check_body_contains(url, "<title>Tilde Friends</title>");
@ -896,6 +900,7 @@ static void _test_httpd(const tf_test_options_t* options)
snprintf(url, sizeof(url), "http://localhost:8080/%s/hello.txt", app_id); snprintf(url, sizeof(url), "http://localhost:8080/%s/hello.txt", app_id);
_http_check_body_contains(url, "Hello, world!"); _http_check_body_contains(url, "Hello, world!");
tf_free((void*)app_id); tf_free((void*)app_id);
#endif
uv_process_kill(&process, SIGTERM); uv_process_kill(&process, SIGTERM);
uv_close((uv_handle_t*)&process, NULL); uv_close((uv_handle_t*)&process, NULL);
@ -920,8 +925,8 @@ static void _test_pattern(const tf_test_options_t* options)
static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal) static void _test_auto_process_exit(uv_process_t* process, int64_t status, int termination_signal)
{ {
tf_printf("Process exit %d signal=%d.\n", (int)WEXITSTATUS(status), termination_signal); tf_printf("Process exit %" PRId64 " signal=%d.\n", status, termination_signal);
assert(WEXITSTATUS(status) == 0); assert(status == 0);
process->data = NULL; process->data = NULL;
uv_close((uv_handle_t*)process, NULL); uv_close((uv_handle_t*)process, NULL);
} }
@ -958,6 +963,7 @@ static void _test_auto(const tf_test_options_t* options)
int spawn_result = uv_spawn(&loop, &process, &process_options); int spawn_result = uv_spawn(&loop, &process, &process_options);
if (spawn_result) if (spawn_result)
{ {
tf_printf("uv_spawn: %s\n", uv_strerror(spawn_result));
abort(); abort();
} }
@ -970,6 +976,7 @@ static void _test_auto(const tf_test_options_t* options)
spawn_result = uv_spawn(&loop, &selenium, &process_options); spawn_result = uv_spawn(&loop, &selenium, &process_options);
if (spawn_result) if (spawn_result)
{ {
tf_printf("uv_spawn: %s\n", uv_strerror(spawn_result));
abort(); abort();
} }

View File

@ -350,6 +350,14 @@ static JSValue _util_defaultGlobalSettings(JSContext* context, JSValueConst this
{ .name = "room_name", .type = "string", .description = "Name of the room.", .default_value = JS_NewString(context, "tilde friends tunnel") }, { .name = "room_name", .type = "string", .description = "Name of the room.", .default_value = JS_NewString(context, "tilde friends tunnel") },
{ .name = "seeds_host", .type = "string", .description = "Hostname for seed connections.", .default_value = JS_NewString(context, "seeds.tildefriends.net") }, { .name = "seeds_host", .type = "string", .description = "Hostname for seed connections.", .default_value = JS_NewString(context, "seeds.tildefriends.net") },
{ .name = "account_registration", .type = "boolean", .description = "Allow registration of new accounts.", .default_value = JS_TRUE }, { .name = "account_registration", .type = "boolean", .description = "Allow registration of new accounts.", .default_value = JS_TRUE },
{ .name = "replication_hops",
.type = "integer",
.description = "Number of hops to replicate (1 = direct follows, 2 = follows of follows, etc.).",
.default_value = JS_NewInt32(context, 2) },
{ .name = "delete_stale_feeds",
.type = "boolean",
.description = "Periodically delete feeds that visible from local accounts and related follows.",
.default_value = JS_FALSE },
}; };
JSValue settings = JS_NewObject(context); JSValue settings = JS_NewObject(context);

View File

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

View File

@ -58,6 +58,32 @@ try:
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click() driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'create_identity').click()
wait.until(expected_conditions.alert_is_present()).accept()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'id_dropdown').find_element(By.XPATH, '//button[position()=2]').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
# NoSuchShadowRootException
while True:
try:
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
break
except:
pass
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'edit_profile').click()
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'name').send_keys('user')
wait.until(exists_in_shadow_root(wait.until(exists_in_shadow_root(tf_app, By.ID, 'tf-tab-news')).shadow_root, By.CLASS_NAME, 'tf-profile')).shadow_root.find_element(By.ID, 'save_profile').click()
driver.switch_to.default_content()
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//button[text()="✅ Allow"]'))).click()
driver.switch_to.default_content()
driver.get('http://localhost:8888/~testuser/test/') driver.get('http://localhost:8888/~testuser/test/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
while True: while True:
@ -71,13 +97,18 @@ try:
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click() driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content') editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
editor.click() editor.click()
editor.send_keys('app.setDocument("<div id=\'test-div\'>Hello, world!</div>")'); editor.send_keys('app.setDocument(\n\t"<div id=\'test-div\'>Hello, world!</div>"\n);');
driver.find_element(By.ID, 'save').click() driver.find_element(By.ID, 'save').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div')))
size = driver.get_window_size()
driver.set_window_size(1200, 540)
driver.save_screenshot('out/screenshot0.png')
driver.set_window_size(size['width'], size['height'])
driver.switch_to.default_content() driver.switch_to.default_content()
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content') editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
editor.click() editor.click()
@ -101,17 +132,20 @@ try:
break break
except: except:
pass pass
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
driver.get('http://localhost:8888') driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))) wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document')) driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'create_identity').click()
wait.until(expected_conditions.alert_is_present()).accept()
driver.switch_to.frame(driver.find_element(By.ID, 'document')) wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
size = driver.get_window_size()
driver.set_window_size(540, 1200)
driver.save_screenshot('out/screenshot1.png')
driver.set_window_size(size['width'], size['height'])
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity'))).click() wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity'))).click()
# StaleElementReferenceException # StaleElementReferenceException