Compare commits

...

96 Commits

Author SHA1 Message Date
7f87714b58 ssb: Add some missing padding.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 27m0s
2025-01-05 21:43:26 -05:00
5594bee618 ssb: Add get_identity, get_sequence, and get_profile commands. #89
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 25m41s
2025-01-05 17:16:05 -05:00
c469ef23e6 ssb: Make the profile layout a little friendlier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m5s
2025-01-05 15:49:57 -05:00
f6e74f2526 ssb: Try to fix profile layout yet again. Man, CSS. 2025-01-05 15:41:56 -05:00
10b6e9c537 ssb: Remove the weird option to make the server account follow you. Now that this account is admin-controlled, it's unnecessary.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 25m58s
2025-01-05 15:29:29 -05:00
3f27af30b7 ssb: Actually fall back to this default global setting value. 2025-01-05 15:17:41 -05:00
23db09f9b7 core: Default to loading into the ssb app. No more messing around.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m3s
2025-01-05 14:52:27 -05:00
d1b7681efc ssb: Don't show 'Loading...' forever in the ssb app when not signed in. Direct to the login page.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 26m50s
2025-01-05 12:52:52 -05:00
61ad405ad8 ssb: Too bright.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m30s
2025-01-04 21:38:52 -05:00
aff98110e0 ssb: Tidy up some of the more common reasons for disconnect.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-04 21:37:43 -05:00
2f36db9142 ssb: Don't display mentions for tags that are used in the text of a message.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m22s
2025-01-04 21:05:24 -05:00
aa86ee1066 cleanup: rm test.c
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m45s
2025-01-04 17:12:00 -05:00
dbbcce8165 ssb: Don't store connections that aren't user-initiated.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-04 17:08:36 -05:00
1ed066ef0f ssb: Fix naked hashtag links (to the corresponding channel).
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m53s
2025-01-04 13:03:58 -05:00
763f7d45d8 ssb: Prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-04 12:41:04 -05:00
2328f3afb5 ssb: Ease up on excessively re-hitting the database for ebt.replicate even more.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m20s
2025-01-04 09:58:16 -05:00
2223245861 ssb: Maybe ease up on hammering the db for follows.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m52s
2025-01-03 18:17:54 -05:00
36226b01cd ssb: Manage new message handling from the new EBT code.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m57s
2025-01-03 17:04:38 -05:00
da31f9cadd cleanup: get/set sent clock is now unused. 2025-01-03 16:56:04 -05:00
9da4857066 ssb: Make the client a bit less aggressive about determining private messages every load.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m56s
2025-01-03 15:53:19 -05:00
75c71135ba ssb: No longer replicate every account we hear about. 2025-01-03 15:25:59 -05:00
0cb5025a16 core: Improve global setting grammar. 2025-01-03 14:10:27 -05:00
44d9f69434 ssb: Refactoring EBT implementation. I think this works not worse than before and will let me schedule message replication better in a future change. #93 2025-01-03 13:59:25 -05:00
3f343b283b ssb: Delete one more redundant global setting accessor. 2025-01-03 08:41:13 -05:00
03a28fc3c5 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m57s
2025-01-03 08:19:41 -05:00
3513619221 ssb: Reduce message margins.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m25s
2025-01-02 21:16:55 -05:00
0c9f5769d3 build: Trying to fix flatpak build for some reason.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m30s
2025-01-02 17:45:27 -05:00
587a666ab6 build: Needed out/ earlier for ssl-local.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-02 17:40:29 -05:00
f26deea508 build: mkdir out/openssl-local.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-02 17:35:49 -05:00
b8e19040b5 ssb: Fiddling with render of encrypted messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m29s
2025-01-02 16:11:04 -05:00
7d9e0f4080 macos: Fix build.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m44s
2025-01-02 14:20:58 -05:00
16ce7fbc7b ssb: Fix the message encrypted icon placement.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m57s
2025-01-02 13:55:47 -05:00
639fce376a ssb: More uv_async_send paranoia still. #96
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m51s
2025-01-02 13:01:09 -05:00
3cdbac5c22 build: Archive the windows .exe with data. 2025-01-02 13:00:42 -05:00
3dcafdf403 ssb: More uv_async_send paranoia. #96
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m52s
2025-01-02 12:40:11 -05:00
cd2fe9f8d9 ssb: Fix a crash on Windows when we would call uv_async_send on a handle that had already been closed. Various other cleanup and improvements along the journey. #96
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-02 12:35:58 -05:00
fd40596ce7 ssb: Every now and then I load in Chrome and see everywhere I used overflow: scroll when I wanted overflow: auto.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m37s
2025-01-02 08:58:10 -05:00
7ecda69703 ssb: tags never got an updated CSS treatment.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-02 08:49:30 -05:00
a3b76cd5c2 core: Let's try getting crash callstacks on win32 with a vectored exception handler.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-02 08:32:18 -05:00
54df862998 ssb: Continuing to untangle message CSS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m42s
2025-01-01 16:44:16 -05:00
301b7a4911 ssb: Trying to untangle some message formatting ugliness. First step: some minor refactoring.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m53s
2025-01-01 15:45:11 -05:00
e0a048abe6 follow: This app had never been updated since jsonb, whoops. #94
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-01-01 15:28:19 -05:00
671e3e19ff ssb: Try to stop dates from wrapping into a vertical line of single characters.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m9s
2024-12-31 08:39:02 -05:00
0c394c2e61 ssb: Trying to keep things CSS-ing off the screen.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m51s
2024-12-30 07:13:49 -05:00
4ecbb5234c ssb: Tweaking profile card CSS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m31s
2024-12-29 15:51:51 -05:00
98f1700049 ssb: Fiddling with message card and compose CSS some more.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 15:42:12 -05:00
2f0b4a0187 ssb: Choose an unread notification that is a bit mire color-agnostic.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m39s
2024-12-29 15:15:28 -05:00
f66c6ed0c3 ssb: Fiddle with the placement of the hamburger menu, and fix the tf-compose placeholder text. 2024-12-29 15:11:14 -05:00
5d9785ac2d ssb: Why did this vertical alignment change? I will never know, but I can poke it back.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 14:59:12 -05:00
bb97a8cccc ssb: Show connections in the sidebar. Fiddle with tf-user CSS to make it fit.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 14:54:29 -05:00
571cf5b5b8 ssb: Color scheme is determined by day/hour/second. Couldn't help it.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m19s
2024-12-29 13:55:48 -05:00
1974ed1c03 ssb: We don't have to wait for channel status to finish load.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 13:41:57 -05:00
98275f7c87 cleanup: Remove a debug print.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 13:34:51 -05:00
eca8726909 ssb: Load and show new messages as they arrive.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-29 13:32:37 -05:00
baf125c450 ssb: Trying to get the sidebar to behave better. Fighting CSS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 24m25s
2024-12-29 12:44:42 -05:00
efcc710d91 ssb: Let's assume we read our own messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m21s
2024-12-28 21:31:24 -05:00
5980ee4c86 cleanup: Unneeded #include.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-28 21:13:53 -05:00
db9b7a22c2 core: Report the c-ares version. 2024-12-28 20:38:07 -05:00
5e24d4f322 build: Support Xcode 16.2 on Linux, including cross-compiling OpenSSL.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m11s
2024-12-27 21:32:33 -05:00
2dd32cdce2 ssb: Tweak idle scheduling even more, still. Fixes -t=bench.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m36s
2024-12-27 15:51:33 -05:00
9cddd93dad ssb: Don't schedule duplicate history stream requests for the same account. Changes how we schedule idle work. Let's see if this is better.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m35s
2024-12-27 15:02:52 -05:00
4127898655 ssb: Avoid more work on shutdown. 2024-12-27 14:27:52 -05:00
45d48483d0 ssb: Avoid scheduling idle work while shutting down, so that we shut down sooner. 2024-12-27 14:03:14 -05:00
852c25296a ssb: Better errors for failing to decrypt private messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m32s
2024-12-27 13:38:09 -05:00
aea631138e ssb: Fix private messages starting with unread status when there are none. 2024-12-27 13:25:40 -05:00
683fdbb02a ssb: Fix channel status not updating reliably.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m2s
2024-12-27 13:23:29 -05:00
c3bbab35e2 ssb: Fix global settings defaults.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-27 13:16:11 -05:00
ba8941046e ssb: Consolidate some redundant connection code.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m32s
2024-12-27 12:27:54 -05:00
d202f4e00d auth: Provide some feedback about valid account names. #92
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 23m8s
2024-12-27 11:39:38 -05:00
42da5d8d32 ssb: Experimenting with generating the w3 css theme colors.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m38s
2024-12-26 22:24:52 -05:00
5af3533598 tests: Clean up some warnings by avoiding in-memory databases. I never got that working well, and it's not representative of actual operation.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m42s
2024-12-26 20:17:17 -05:00
7843168fad ssb: Hook up some more disconnect messaging. 2024-12-26 20:12:04 -05:00
8f51eb63b0 ssb: More experimenting with the unread status strategy.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m35s
2024-12-26 19:36:04 -05:00
855f5f7af4 ssb: Before destroying a connection, show a message on why it is going away in the UI.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m6s
2024-12-24 17:23:22 -05:00
c85dd2655c ssb: Strip out the old disconnection debug information.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m15s
2024-12-24 16:44:52 -05:00
fb0e4060cd ssb: Instrument some more callbacks for hitches.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-24 16:33:08 -05:00
707b4990a6 build: Not all the toolchains support -Oz. Oh well.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m3s
2024-12-24 15:01:09 -05:00
9c8b922069 build: Use all the tricks to make release smaller on all the platforms.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 2m15s
2024-12-24 14:47:33 -05:00
d4b421421d ssb: prettier. 2024-12-24 14:22:24 -05:00
58e9646fa6 ssb: Populate reply information in posts.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m33s
2024-12-24 13:09:27 -05:00
500f172561 ssb: Double down on a loading indicator.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m29s
2024-12-24 12:45:25 -05:00
68f6c90ea4 ssb: Get saving the about cache off of the main load path.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m31s
2024-12-24 12:05:31 -05:00
41e91f2922 ssb: Consolidate global settings helpers.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m28s
2024-12-24 11:16:52 -05:00
999117cfeb clean: Remove a file I sloppily added. 2024-12-24 10:45:47 -05:00
6185df512f build: Add help for armdebug/release, and fix some make help alignment. 2024-12-24 10:39:11 -05:00
0cbf66c007 build: Let's try to artifact the x86_64 + ARM linux executables.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m30s
2024-12-24 10:31:09 -05:00
cd378b721d build: Support and test cross-compiling for linux-aarch64.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m13s
2024-12-24 10:01:14 -05:00
547d38d1ef ssb: Put the database in (ie, ~/.local/share/tildefriends/db.sqlite) by default. Unless it already exists in the working directory, so that nobody worries they've lost it. #91
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m48s
2024-12-23 16:32:30 -05:00
dca56af5b9 build: Let's go static openssl on macos, too.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m0s
2024-12-23 14:41:31 -05:00
224442772e build: Let's try a thing. Use our own static openssl libraries built in-tree.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m49s
2024-12-23 14:22:27 -05:00
003951fdf7 ssb: Load more context for mentions.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m39s
2024-12-23 13:32:36 -05:00
d51b3da1b4 ssb: Fix channel cycling key shortcut.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-23 13:18:30 -05:00
69f4af84db ssb: Fix weird sidebar sizing.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-23 13:07:33 -05:00
771759b252 ssb: Show drafts in the sidebar.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m22s
2024-12-23 12:25:52 -05:00
20c7a71db6 ssb: Add a checkbox to reply in a new thread. #47 Also, it's crab time. I'm sorry it took me so long.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m53s
2024-12-23 12:06:32 -05:00
8475ee0985 build: Let's start work on 0.0.27. 2024-12-23 11:23:51 -05:00
45 changed files with 2438 additions and 1635 deletions

View File

@ -24,15 +24,14 @@ 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 - run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11 gcc-aarch64-linux-gnu
- 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: out/TildeFriends-release.fdroid.apk path: |
- uses: actions/upload-artifact@v3 out/TildeFriends-release.fdroid.apk
with: out/winrelease/tildefriends.standalone.exe
path: out/winrelease/tildefriends.exe out/tildefriends-x86_64.AppImage
- uses: actions/upload-artifact@v3 out/release/tildefriends.standalone
with: out/armrelease/tildefriends.standalone
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 := 31 VERSION_CODE := 32
VERSION_NUMBER := 0.0.26 VERSION_NUMBER := 0.0.27-wip
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,6 +34,7 @@ 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
@ -45,6 +46,9 @@ 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
@ -123,6 +127,14 @@ 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 := \
@ -149,6 +161,9 @@ 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 \
@ -159,7 +174,8 @@ 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 \
@ -169,7 +185,8 @@ 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))
@ -192,11 +209,14 @@ $(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 += \
@ -208,6 +228,10 @@ $(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)
@ -215,7 +239,8 @@ $(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): IOS_SYSROOT := deps/iPhoneOS17.0.sdk $(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64
$(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
@ -238,14 +263,23 @@ $(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 -Ideps/openssl/ios/ios64-xcrun/usr/local/include $(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib $(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0
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 all: appimage out/release/tildefriends.standalone
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
@ -260,7 +294,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,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \ $(foreach build_type,debug release armdebug armrelease,$(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))))) \
@ -725,7 +759,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \ LDFLAGS += \
-pthread \ -pthread \
-lm -lm
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ $(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS): LDFLAGS += \
-lssl \ -lssl \
-lcrypto -lcrypto
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
@ -765,6 +799,8 @@ $(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.
@ -1056,6 +1092,7 @@ 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
@ -1106,6 +1143,42 @@ $(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):
@ -1198,7 +1271,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 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)
@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
@ -1216,6 +1289,10 @@ 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.
@ -1258,7 +1335,7 @@ help: ## Display this help message.
/^##/ { sub(/^## ?/, ""); print $$0 } \ /^##/ { sub(/^## ?/, ""); print $$0 } \
/^[[:alnum:]-]+:.*##/ { \ /^[[:alnum:]-]+:.*##/ { \
sub(/:.*##\s?/, ":"); \ sub(/:.*##\s?/, ":"); \
printf " %s%-20s%s %s%s%s\n", G, $$1, R, O, $$2, R \ printf " %s%-21s%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,4 +1,5 @@
{ {
"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 content FROM messages SELECT json(content) AS content FROM messages
WHERE author = ? AND WHERE author = ? AND
rowid > ? AND rowid > ? AND
rowid <= ? AND rowid <= ? AND
@ -189,50 +189,6 @@ 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": "&q/1uGp0jMvsYGW7Gj8E33kf6UFo/uNYDXg3zo1sVKQg=.sha256" "previous": "&jbL9Ab+XdvWnZbb50yimceFHR7XFDfBSWv9/XrbZ82I=.sha256"
} }

View File

@ -21,9 +21,6 @@ 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();
}); });
@ -106,6 +103,10 @@ 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,7 +7,6 @@ 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},
@ -19,6 +18,8 @@ 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},
}; };
} }
@ -28,7 +29,6 @@ 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,7 +68,9 @@ 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();
} }
@ -112,26 +114,24 @@ 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 = ['', '@'].concat(this.channels); let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
let index = channel_names.indexOf(this.hash.substring(1)); let index = channel_names.indexOf(this.hash.substring(1));
if (index != -1) { index = index != -1 ? index + delta : 0;
index += delta; tfrpc.rpc.setHash(
this.set_hash( '#' +
'#' + encodeURIComponent(
encodeURIComponent( channel_names[(index + channel_names.length) % channel_names.length]
channel_names[(index + channel_names.length) % channel_names.length] )
) );
);
}
} }
set_hash(hash) { set_hash(hash) {
@ -150,6 +150,7 @@ 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 = {
@ -215,7 +216,13 @@ class TfElement extends LitElement {
} }
} }
cache.last_row_id = max_row_id; cache.last_row_id = max_row_id;
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache)); let new_cache = 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]);
@ -234,17 +241,13 @@ 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 (message.author == this.whoami) { if (
let content = JSON.parse(message.content); message.author == this.whoami &&
if (content?.type == 'channel') { JSON.parse(message.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();
} }
@ -271,31 +274,74 @@ 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;
const k_chunk_count = 256; let ranges = [];
while (latest - k_chunk_count >= 0) { const k_chunk_size = 512;
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, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT messages.rowid, messages.id, json(content) AS content
FROM messages FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE WHERE
messages.rowid > ?2 AND messages.rowid > ?1 AND
messages.rowid <= ?3 AND messages.rowid <= ?2 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY sequence DESC ORDER BY sequence DESC
`, `,
[JSON.stringify(following), latest - k_chunk_count, latest] [range[0], range[1]]
); );
messages = (await this.decrypt(messages)).filter((x) => x.decrypted); messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
if (messages.length) { if (messages.length) {
return Math.max(...messages.map((x) => x.rowid)); cache.latest = Math.max(
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];
}
} }
latest -= k_chunk_count; cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
await tfrpc.rpc.databaseSet(
`private:${this.whoami}`,
JSON.stringify(cache)
);
} }
return -1; console.log(cache);
return cache.latest;
} }
async load_channels_latest(following) { async load_channels_latest(following) {
@ -308,20 +354,29 @@ 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 messages.content ->> 'type' = 'post' AND messages.content ->> 'root' IS NULL WHERE
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(
@ -349,14 +404,16 @@ 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, 5000); setTimeout(this._schedule_load_channels_latest_timer.bind(this), 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 = {};
@ -370,11 +427,10 @@ class TfElement extends LitElement {
}; };
by_count.push({count: v.of, id: id}); by_count.push({count: v.of, id: id});
} }
let channels_latest = this.load_channels_latest(Object.keys(following)); 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',
@ -383,11 +439,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(`load finished ${whoami} => ${this.whoami}`); console.log(
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
this.whoami = whoami; this.whoami = whoami;
this.loaded = whoami; this.loaded = whoami;
} }
@ -435,13 +491,12 @@ 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') {
@ -512,7 +567,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: sticky; top: 0; z-index: 10" style="position: static; 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"
@ -538,22 +593,36 @@ class TfElement extends LitElement {
)} )}
</div> </div>
`; `;
let contents = !this.loaded let contents = this.guest
? 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 w3-container"
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge" >
<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
> >
Loading... </footer>
</div> </div>`
${this.render_tab()}` : !this.loaded || this.loading
: html`<div>Select or create an identity.</div>` ? html`<div
: this.render_tab(); class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
>
<span class="w3-spin" style="display: inline-block">🦀</span>
Loading...
</div>`
: this.render_tab();
return html` return html`
<div <div
style="width: 100vw; min-height: 100vh; height: 100%" style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark" class="w3-theme-dark"
> >
${tabs} ${contents} <div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout">
${contents}
</div>
</div> </div>
`; `;
} }

View File

@ -15,6 +15,7 @@ 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},
}; };
} }
@ -28,6 +29,7 @@ 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) {
@ -200,9 +202,23 @@ class TfComposeElement extends LitElement {
channel: this.channel, channel: this.channel,
}; };
if (this.root || this.branch) { if (this.root || this.branch) {
message.root = this.root; message.root = this.new_thread ? (this.branch ?? this.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);
} }
@ -469,6 +485,20 @@ 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 || ''] || {};
} }
@ -533,14 +563,24 @@ 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-small" class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom"
style="box-sizing: border-box" style="box-sizing: border-box"
> >
${this.channel !== undefined <header class="w3-container">
? html`<p>To #${this.channel}:</p>` ${this.channel !== undefined
: undefined} ? html`<p>To #${this.channel}:</p>`
${this.render_encrypt()} : undefined}
${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
@ -554,25 +594,32 @@ class TfComposeElement extends LitElement {
.innerText=${live(draft.text ?? '')} .innerText=${live(draft.text ?? '')}
></span> ></span>
</div> </div>
<div class="w3-half w3-padding"> <div class="w3-half">
${content_warning} ${content_warning}
<div id="preview"></div> <p id="preview"></p>
</div> </div>
</div> </div>
${Object.values(draft.mentions || {}).map((x) => ${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x) self.render_mention(x)
)} )}
${this.render_attach_app()} ${this.render_content_warning()} <footer class="w3-container">
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}> ${this.render_attach_app()} ${this.render_content_warning()}
Submit ${this.render_new_thread()}
</button> <button
<button class="w3-button w3-theme-d1" @click=${this.attach}> class="w3-button w3-theme-d1"
Attach id="submit"
</button> @click=${this.submit}
${this.render_attach_app_button()} ${encrypt} >
<button class="w3-button w3-theme-d1" @click=${this.discard}> Submit
Discard </button>
</button> <button class="w3-button w3-theme-d1" @click=${this.attach}>
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,6 +97,13 @@ 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,
@ -108,9 +115,7 @@ class TfMessageElement extends LitElement {
content: this.message?.content, content: this.message?.content,
signature: this.message?.signature, signature: this.message?.signature,
}; };
return html`<div style="white-space: pre-wrap"> return this.render_json(raw);
${JSON.stringify(raw, null, 2)}
</div>`;
} }
vote(emoji) { vote(emoji) {
@ -190,7 +195,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 html` <pre>${JSON.stringify(mention)}</pre>`; return this.render_json(mention);
} else if ( } else if (
mention?.link?.startsWith('&') && mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/') mention?.type?.startsWith('image/')
@ -241,16 +246,17 @@ 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 html` <pre style="white-space: pre-wrap"> return this.render_json(mention);
${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) => this.message?.content?.text?.indexOf(x.link) === -1 (x) =>
this.message?.content?.text?.indexOf(
typeof x === 'string' ? x : x.link
) === -1
); );
if (mentions.length) { if (mentions.length) {
let self = this; let self = this;
@ -320,6 +326,8 @@ ${JSON.stringify(mention, null, 2)}</pre
></tf-message>` ></tf-message>`
)}`; )}`;
} }
} else {
return undefined;
} }
} }
@ -355,31 +363,38 @@ ${JSON.stringify(mention, null, 2)}</pre
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`); return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
} }
render() { class_background() {
let content = this.message?.content; return this.message?.decrypted
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red' ? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread : this.message?.rowid >= this.channel_unread
? 'w3-theme-d2' ? 'w3-theme-d2'
: 'w3-theme-d4'; : 'w3-theme-d4';
let self = this; }
get_content() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
return content;
}
render_raw_button() {
let content = this.get_content();
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=${() => (self.format = 'md')} @click=${() => (this.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=${() => (self.format = 'message')} @click=${() => (this.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -388,7 +403,7 @@ ${JSON.stringify(mention, null, 2)}</pre
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=${() => (self.format = 'message')} @click=${() => (this.format = 'message')}
> >
Message Message
</button>`; </button>`;
@ -396,7 +411,7 @@ ${JSON.stringify(mention, null, 2)}</pre
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=${() => (self.format = 'raw')} @click=${() => (this.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
@ -405,58 +420,136 @@ ${JSON.stringify(mention, null, 2)}</pre
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=${() => (self.format = 'decrypted')} @click=${() => (this.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=${() => (self.format = 'raw')} @click=${() => (this.format = 'raw')}
> >
Raw Raw
</button>`; </button>`;
} }
break; break;
} }
function small_frame(inner) { return raw_button;
let body; }
return html`
<div render_header() {
class="w3-card-4 ${class_background} w3-border-theme" let is_encrypted = this.message?.decrypted
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere" ? html`<span class="w3-bar-item">🔓</span>`
> : typeof this.message?.content == 'string'
<tf-user id=${self.message.author} .users=${self.users}></tf-user> ? html`<span class="w3-bar-item">🔒</span>`
<span style="padding-right: 8px" : undefined;
><a tfarget="_top" href=${'#' + encodeURIComponent(self.message.id)} return html`
>%</a <header class="w3-bar">
> <span class="w3-bar-item">
${new Date(self.message.timestamp).toLocaleString()}</span <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
> >
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner} ${new Date(this.message.timestamp).toLocaleString()}</span
${self.render_votes()} >
${(self.message.child_messages || []).map( </header>
(x) => html` `;
<tf-message }
.message=${x}
whoami=${self.whoami} render_frame(inner) {
.users=${self.users} return html`
.drafts=${self.drafts} <style>
.expanded=${self.expanded} code {
channel=${self.channel} white-space: pre-wrap;
channel_unread=${self.channel_unread} overflow-wrap: break-word;
></tf-message> }
` div {
)} overflow-wrap: anywhere;
</div> }
`; img {
} max-width: 100%;
if (this.message?.type === 'contact_group') { height: auto;
return html` <div display: block;
class="w3-card-4 ${class_background} w3-border-theme" }
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere" </style>
<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%"
> >
${this.message.messages.map( ${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') {
return this.render_frame(
html` ${this.message.messages.map(
(x) => (x) =>
html`<tf-message html`<tf-message
.message=${x} .message=${x}
@ -467,39 +560,38 @@ ${JSON.stringify(mention, null, 2)}</pre
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 html` <div return this.render_frame(
class="w3-card-4 ${class_background} w3-border-theme" html` <a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere" >${this.message.id}</a
> >
<a target="_top" href=${'#' + encodeURIComponent(this.message.id)} (placeholder)
>${this.message.id}</a <div>${this.render_votes()}</div>
> ${(this.message.child_messages || []).map(
(placeholder) (x) => html`
<div>${this.render_votes()}</div> <tf-message
${(this.message.child_messages || []).map( .message=${x}
(x) => html` whoami=${this.whoami}
<tf-message .users=${this.users}
.message=${x} .drafts=${this.drafts}
whoami=${this.whoami} .expanded=${this.expanded}
.users=${this.users} channel=${this.channel}
.drafts=${this.drafts} channel_unread=${this.channel_unread}
.expanded=${this.expanded} ></tf-message>
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><b>Name:</b> ${content.name}</div>`; name = html`<div>
<b>Name:</b> ${content.name}
</div>`;
} }
if (content.image !== undefined) { if (content.image !== undefined) {
image = html` image = html`
@ -508,22 +600,30 @@ ${JSON.stringify(mention, null, 2)}</pre
} }
if (content.description !== undefined) { if (content.description !== undefined) {
description = html` description = html`
<div style="flex: 1 0 50%; overflow-wrap: anywhere"> <div
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">Updated profile.</div>` ? html`<div style="font-weight: bold">
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 small_frame(html` ${update} ${name} ${image} ${description} `); return this.render_small_frame(html`
<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> <div class="w3-padding">
<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
@ -542,24 +642,6 @@ ${JSON.stringify(mention, null, 2)}</pre
</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) {
@ -576,11 +658,7 @@ ${JSON.stringify(mention, null, 2)}</pre
body = unsafeHTML(tfutils.markdown(content.text)); body = unsafeHTML(tfutils.markdown(content.text));
break; break;
case 'decrypted': case 'decrypted':
body = html`<pre body = this.render_json(content);
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break; break;
} }
let content_warning = html` let content_warning = html`
@ -602,108 +680,22 @@ ${JSON.stringify(content, null, 2)}</pre
? html` ${content_warning} ${content_html} ` ? html` ${content_warning} ${content_html} `
: content_warning : content_warning
: content_html; : content_html;
let is_encrypted = this.message?.decrypted return this.render_frame(html`
? html`<span style="align-self: center">🔓</span>` ${this.render_header()}
: undefined; <div class="w3-container">${payload}</div>
return html` ${this.render_votes()} ${this.render_actions()}
<style> </div>
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>
${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()}
</div>
`;
} else if (content.type === 'issue') { } else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted return this.render_frame(html`
? html`<span style="align-self: center">🔓</span>` ${this.render_header()} ${content.text} ${this.render_votes()}
: undefined; <footer class="w3-container">
return html` <button class="w3-button w3-theme-d1" @click=${this.react}>
<style> React
code { </button>
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()} ${this.render_children()}
</div> </footer>
`; `);
} 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) {
@ -739,70 +731,14 @@ ${JSON.stringify(content, null, 2)}</pre
`; `;
break; break;
} }
let reply = return this.render_frame(html`
this.drafts[this.message?.id] !== undefined ${this.render_header()}
? html` <div>${body}</div>
<tf-compose ${this.render_mentions()} ${this.render_votes()}
whoami=${this.whoami} ${this.render_actions()}
.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 small_frame( return this.render_small_frame(
html` <style> html` <style>
span { span {
overflow-wrap: anywhere; overflow-wrap: anywhere;
@ -820,35 +756,42 @@ ${JSON.stringify(content, null, 2)}</pre
</span>` </span>`
); );
} else if (content.type === 'channel') { } else if (content.type === 'channel') {
return small_frame(html` return this.render_small_frame(html`
<div> <div class="w3-container">
<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 small_frame( return this.render_small_frame(
html`<span>🔓</span> html`<span class="w3-container">🔓</span> ${this.render_json(
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>` this.message.decrypted
)}`
); );
} else { } else {
return small_frame( return this.render_small_frame(
html`<span>🔓</span> html`<span class="w3-container">🔓</span>
<div>${this.message.decrypted.type}</div>` <div class="w3-container">${this.message.decrypted.type}</div>`
); );
} }
} else { } else {
return small_frame(html`<span>🔒</span>`); return this.render_small_frame();
} }
} else { } else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`); return this.render_small_frame(
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 small_frame(this.render_raw()); return this.render_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 != final_messages[0] ${x.rowid == unread_rowid
? 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,7 +11,6 @@ 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},
}; };
@ -27,7 +26,6 @@ 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() {
@ -63,26 +61,6 @@ 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(
@ -175,31 +153,11 @@ 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] || {};
@ -216,22 +174,6 @@ 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"
@ -243,7 +185,6 @@ 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
@ -276,20 +217,18 @@ 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 class="w3-container"> <div>
<div> <label for="name">Name:</label>
<label for="name">Name:</label> <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>
<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>
</div> <div><label for="description">Description:</label></div>
<div><label for="description">Description:</label></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>
<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> <div>
<div> <label for="public_web_hosting">Public Web Hosting:</label>
<label for="public_web_hosting">Public Web Hosting:</label> <input class="w3-check w3-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>
<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> <div>
<div> <button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div> </div>
</div>` </div>`
: null; : null;
@ -297,28 +236,36 @@ 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-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)"> return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)}) <header class="w3-container">
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input> <p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p>
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button> </header>
<div style="display: flex; flex-direction: row; gap: 1em"> <div class="w3-container">
${edit_profile} <div class="w3-margin-bottom" style="display: flex; flex-direction: row">
<div style="flex: 1 0 50%"> <input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div> <button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
<div>${unsafeHTML(tfutils.markdown(description))}</div> </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>
<div> <footer class="w3-container">
Following ${profile.following} identities. <p>
Followed by ${profile.followed} identities. ${edit}
Blocking ${profile.blocking} identities. ${follow}
Blocked by ${profile.blocked} identities. ${block}
</div> </p>
<p> </footer>
${edit}
${follow}
${block}
</p>
</div>`; </div>`;
} }
} }

View File

@ -1,4 +1,4 @@
import {css} from './lit-all.min.js'; import {css, unsafeCSS} from './lit-all.min.js';
const tf = css` const tf = css`
img { img {
@ -285,30 +285,165 @@ 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}
`; `;
// prettier-ignore function rgb_to_hsl(r, g, b) {
const w3_2016_snorkel_blue = css` let min,
.w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important} max,
.w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important} i,
.w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important} l,
.w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important} s,
.w3-theme-l1 {color:#fff !important; background-color:#0479cc !important} maxcolor,
.w3-theme-d1 {color:#fff !important; background-color:#024575 !important} h,
.w3-theme-d2 {color:#fff !important; background-color:#023e68 !important} rgb = [];
.w3-theme-d3 {color:#fff !important; background-color:#02365b !important} rgb[0] = r / 255;
.w3-theme-d4 {color:#fff !important; background-color:#022e4e !important} rgb[1] = g / 255;
.w3-theme-d5 {color:#fff !important; background-color:#012641 !important} rgb[2] = b / 255;
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];
}
.w3-theme-light {color:#000 !important; background-color:#e9f5ff !important} function hex_to_rgb(hex) {
.w3-theme-dark {color:#fff !important; background-color:#012641 !important} if (hex.charAt(0) == '#') {
.w3-theme-action {color:#fff !important; background-color:#012641 !important} hex = hex.substring(1);
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
}
.w3-theme {color:#fff !important; background-color:#034f84 !important} function hsl_to_rgb(hue, sat, light) {
.w3-text-theme {color:#034f84 !important} let t2;
.w3-border-theme {border-color:#034f84 !important} hue /= 60;
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;
}
}
.w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important} function rgb_to_hex(rgb) {
.w3-hover-text-theme:hover {color:#034f84 !important} const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0');
.w3-hover-border-theme:hover {border-color:#034f84 !important} return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`;
`; }
export let styles = [tf, w3, w3_2016_snorkel_blue]; function is_dark(hex, value) {
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,6 +175,9 @@ 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,15 +51,21 @@ 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 messages_fts(?1) FROM mentions
JOIN messages ON messages.rowid = messages_fts.rowid JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN json_each(?2) AS following ON messages.author = following.value JOIN messages ON messages_refs.message = messages.id
WHERE UNION
messages.author != ?1 AND SELECT * FROM mentions
messages.timestamp >= ?3 AND
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20
`, `,
[ [
'"' + this.whoami.replace('"', '""') + '"', '"' + this.whoami.replace('"', '""') + '"',
@ -288,7 +294,12 @@ class TfTabNewsFeedElement extends LitElement {
this.loading--; this.loading--;
} }
this.messages = Object.values( this.messages = Object.values(
Object.fromEntries([...this.messages, ...messages].map((x) => [x.id, x])) Object.fromEntries(
[...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,7 +8,6 @@ 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},
@ -16,6 +15,7 @@ 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,7 +27,6 @@ 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 = {};
@ -35,6 +34,7 @@ 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,36 +50,13 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this)); document.body.removeEventListener('keypress', this.on_keypress.bind(this));
} }
show_more() { load_latest() {
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];
@ -112,10 +89,11 @@ 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 '✉️ ';
} }
} }
@ -150,33 +128,11 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
} }
render() { render_sidebar() {
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" style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
id="sidebar" id="sidebar"
> >
<div <div
@ -202,89 +158,144 @@ 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}
>general ${this.unread_status('')}</a >${this.unread_status('')}general</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}
>@mentions ${this.unread_status('@')}</a >${this.unread_status('@')}@mentions</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}
>🔐private ${this.unread_status('🔐')}</a >${this.unread_status('🔐')}🔐private</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}
>#${x} ${this.unread_status(x)}</a >${this.unread_status(x)}#${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"
class="w3-left w3-button w3-hide-large" render() {
@click=${this.show_sidebar} let profile =
> this.hash.startsWith('#@') && this.hash != '#@'
&#9776; ? html`<tf-profile
</div> class="tf-profile"
<p> id=${this.hash.substring(1)}
<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} whoami=${this.whoami}
.users=${this.users} .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} .drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft} @tf-draft=${this.draft}
.channel=${this.channel()} @tf-expand=${this.on_expand}
></tf-compose> .channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed>
</div> </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)}
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px" class="w3-tag w3-theme-d1 w3-round-4 w3-button"
>${this.tag}${number}</a >${this.tag}${number}</a
>`; > `;
} }
} }

View File

@ -25,10 +25,9 @@ class TfUserElement extends LitElement {
>?</span >?</span
>`; >`;
let name = this.users?.[this.id]?.name; let name = this.users?.[this.id]?.name;
name = name = html`<a target="_top" href=${'#' + this.id}
name !== undefined >${name !== undefined ? name : this.id}</a
? html`<a target="_top" href=${'#' + this.id}>${name}</a>` >`;
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
if (this.users[this.id]) { if (this.users[this.id]) {
let image_link = this.users[this.id].image; let image_link = this.users[this.id].image;
@ -42,7 +41,9 @@ class TfUserElement extends LitElement {
/>`; />`;
} }
} }
return html` <div style="display: inline-block; font-weight: bold"> return html` <div
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,19 +577,6 @@ 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.25"; version = "0.0.26";
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-Rfk+CUhi+Ss0z70CCgmtVM/w4nCL1GX/MsD4sPYIa5s="; hash = "sha256-XJ7M++risfsRn9GkS1zjTQpqqV5S09uyimeVzU9hGGg=";
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.7", "version": "6.10.8",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.7.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz",
"integrity": "sha512-aOswhVOLYhMNeqykt4P7+ukQSpGL0ynZYaEyFDVHE7fl2xgluU3yuE9MdgYNfw6EmaNidoFMIQ2iTh1ADrnT6A==", "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
"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.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"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="31" android:versionCode="32"
android:versionName="0.0.26"> android:versionName="0.0.27-wip">
<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,25 +738,6 @@ 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))
@ -2099,19 +2080,26 @@ 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 (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0) if (!_is_name_valid(account_name))
{ {
sqlite3* db = tf_ssb_acquire_db_writer(ssb); login_error = "Invalid username. Usernames must contain only letters from the English alphabet and digits and must start with a letter.";
registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password); }
tf_ssb_release_db_writer(ssb, db); else
if (registered) {
if (!have_account && _is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{ {
tf_free((void*)send_session); sqlite3* db = tf_ssb_acquire_db_writer(ssb);
send_session = _make_session_jwt(context, ssb, account_name); registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password);
may_become_first_admin = true; tf_ssb_release_db_writer(ssb, db);
if (registered)
{
tf_free((void*)send_session);
send_session = _make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
} }
} }
if (!registered) if (!registered && !login_error)
{ {
login_error = "Error registering account."; login_error = "Error registering account.";
} }
@ -2317,7 +2305,6 @@ 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,6 +15,7 @@
#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>
@ -25,6 +26,7 @@
#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__)
@ -41,7 +43,102 @@
struct backtrace_state* g_backtrace_state; struct backtrace_state* g_backtrace_state;
const char* k_db_path_default = "db.sqlite"; #if !TARGET_OS_IPHONE
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[]);
@ -50,6 +147,9 @@ 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_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);
@ -67,6 +167,9 @@ 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." },
{ "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." },
@ -131,7 +234,8 @@ 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* db_path = k_db_path_default; const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -169,11 +273,13 @@ 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", k_db_path_default); 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_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)
{ {
@ -189,13 +295,15 @@ 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* db_path = k_db_path_default; const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -233,10 +341,11 @@ 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", k_db_path_default); 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_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;
} }
@ -271,6 +380,7 @@ 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;
} }
@ -298,7 +408,8 @@ 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* db_path = k_db_path_default; const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
const char* content = NULL; const char* content = NULL;
bool show_usage = false; bool show_usage = false;
@ -346,14 +457,16 @@ 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", k_db_path_default); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
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)))
@ -386,12 +499,14 @@ 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* db_path = k_db_path_default; const char* default_db_path = _get_db_path();
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;
@ -429,9 +544,10 @@ 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", k_db_path_default); tf_printf(" -d, --db-path db_path SQLite database path (default: %s).\n", default_db_path);
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;
} }
@ -441,6 +557,7 @@ 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;
} }
@ -464,11 +581,13 @@ 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))
{ {
@ -476,13 +595,182 @@ 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_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* db_path = k_db_path_default; const char* default_db_path = _get_db_path();
const char* db_path = default_db_path;
bool show_usage = false; bool show_usage = false;
while (!show_usage) while (!show_usage)
@ -520,8 +808,9 @@ 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", k_db_path_default); 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_printf(" -h, --help Show this usage information.\n");
tf_free((void*)default_db_path);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -529,6 +818,7 @@ 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
@ -673,13 +963,14 @@ 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 = k_db_path_default, .db_path = default_db_path,
}; };
bool show_usage = false; bool show_usage = false;
@ -764,7 +1055,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", k_db_path_default); tf_printf(" -d, --db-path path SQLite database path (default: %s).\n", default_db_path);
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");
@ -772,6 +1063,7 @@ 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;
} }
@ -780,6 +1072,7 @@ 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);
@ -807,6 +1100,7 @@ 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;
} }
@ -888,6 +1182,21 @@ 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 };
@ -921,20 +1230,24 @@ static void _startup(int argc, char* argv[])
#endif #endif
bool use_error_handler = false; bool use_error_handler = false;
#if defined(__ANDROID__) #if defined(__ANDROID__) || defined(_WIN32)
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(SIGABRT, _error_handler) == SIG_ERR || signal(SIGSYS, _error_handler) == SIG_ERR ||
#endif #endif
signal(SIGSEGV, _error_handler) == SIG_ERR) signal(SIGABRT, _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__)
@ -1088,7 +1401,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 = k_db_path_default, .db_path = "db.sqlite",
.one_proc = true, .one_proc = true,
.zip = zip_path, .zip = zip_path,
}; };

636
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, 0, NULL, NULL); tf_ssb_connect(ssb, next->host, next->port, key_bin, k_tf_ssb_connect_flag_do_not_store, 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, NULL, NULL); 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);
} }
} }

View File

@ -1681,34 +1681,13 @@ 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 = _tf_ssb_db_get_global_setting_bool(db, "account_registration", true); bool registration_allowed = 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;
@ -1923,6 +1902,12 @@ 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)
@ -2030,3 +2015,98 @@ 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

@ -455,6 +455,42 @@ 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.

322
src/ssb.ebt.c Normal file
View File

@ -0,0 +1,322 @@
#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;
}

99
src/ssb.ebt.h Normal file
View File

@ -0,0 +1,99 @@
#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,12 +71,15 @@ 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. */
@ -514,8 +517,9 @@ 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); void tf_ssb_connection_close(tf_ssb_connection_t* connection, const char* reason);
/** /**
** Check whether a connection is connected. ** Check whether a connection is connected.
@ -524,6 +528,13 @@ void tf_ssb_connection_close(tf_ssb_connection_t* connection);
*/ */
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.
@ -798,17 +809,19 @@ 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, void* user_data); typedef void(tf_ssb_scheduled_callback_t)(tf_ssb_connection_t* connection, bool skip, 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, tf_ssb_scheduled_callback_t* callback, void* user_data); void tf_ssb_connection_schedule_idle(tf_ssb_connection_t* connection, const char* key, 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.
@ -913,20 +926,6 @@ 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
@ -947,14 +946,6 @@ 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.
@ -1101,6 +1092,13 @@ 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.
@ -1123,4 +1121,11 @@ 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,111 +251,6 @@ 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];
@ -1120,6 +1015,11 @@ 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);
} }
} }
@ -1193,7 +1093,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); tf_ssb_connection_close(connection, "Closed by user");
} }
JS_FreeCString(context, id); JS_FreeCString(context, id);
return connection ? JS_TRUE : JS_FALSE; return connection ? JS_TRUE : JS_FALSE;
@ -1490,10 +1390,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_NewPromiseCapability(context, work->promise); JSValue result = JS_UNDEFINED;
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++)
{ {
@ -1536,18 +1436,6 @@ 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;
} }
@ -2234,53 +2122,60 @@ static void _tf_ssb_private_message_decrypt_work(tf_ssb_t* ssb, void* user_data)
if (found) if (found)
{ {
uint8_t* decoded = tf_malloc(work->message_size); if (work->message_size >= strlen(".box") && memcmp(work->message + work->message_size - strlen(".box"), ".box", strlen(".box")) == 0)
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 shared_secret[crypto_secretbox_KEYBYTES] = { 0 }; uint8_t* decoded = tf_malloc(work->message_size);
if (crypto_scalarmult(shared_secret, private_key, public_key) == 0) 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)
{ {
enum uint8_t shared_secret[crypto_secretbox_KEYBYTES] = { 0 };
if (crypto_scalarmult(shared_secret, private_key, public_key) == 0)
{ {
k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES enum
};
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)
{ {
int recipients = (int)out[0]; k_recipient_header_bytes = crypto_secretbox_MACBYTES + sizeof(uint8_t) + crypto_secretbox_KEYBYTES
uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients; };
size_t body_size = decoded + decoded_length - body; for (uint8_t* p = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES; p <= decoded + decoded_length - k_recipient_header_bytes;
uint8_t* decrypted = tf_malloc(body_size); p += k_recipient_header_bytes)
uint8_t* key = out + 1; {
if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1) 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)
{ {
work->decrypted = (const char*)decrypted; int recipients = (int)out[0];
work->decrypted_size = body_size - crypto_secretbox_MACBYTES; uint8_t* body = decoded + crypto_box_NONCEBYTES + crypto_secretbox_KEYBYTES + k_recipient_header_bytes * recipients;
} size_t body_size = decoded + decoded_length - body;
else uint8_t* decrypted = tf_malloc(body_size);
{ uint8_t* key = out + 1;
work->error = "Received key to open secret box containing message body, but it did not work."; if (crypto_secretbox_open_easy(decrypted, body, body_size, nonce, key) != -1)
{
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 = "crypto_scalarmult failed."; work->error = "Encrypted message was not long enough to contain its one-time public key.";
} }
tf_free(decoded);
} }
else else
{ {
work->error = "Encrypted message was not long enough to contains its one-time public key."; work->error = "Message does not end in \".box\".";
} }
tf_free(decoded);
} }
else else
{ {
@ -2562,7 +2457,6 @@ 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,6 +3,7 @@
#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"
@ -19,54 +20,7 @@ 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)
@ -243,7 +197,10 @@ 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 = _get_global_setting_int64(ssb, "blob_fetch_age_seconds", -1); int64_t age = -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)
{ {
@ -256,7 +213,7 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
timestamp = now - age * 1000ULL; timestamp = now - age * 1000ULL;
} }
sqlite3* db = tf_ssb_acquire_db_reader(ssb); 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)
{ {
@ -336,7 +293,29 @@ 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
{ {
@ -874,11 +853,21 @@ 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)) if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)))
{ {
for (int i = 0; i < request->out_messages_count; i++) for (int i = 0; i < request->out_messages_count; i++)
{ {
@ -888,6 +877,7 @@ 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(
@ -898,40 +888,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);
} }
} }
for (int i = 0; i < request->out_messages_count; i++) _tf_ssb_connection_send_history_stream_destroy(request);
{
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, void* user_data) static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t* connection, bool skip, void* 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)) if (!skip && tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(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_after_work(connection, -1, user_data); _tf_ssb_connection_send_history_stream_destroy(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)
{ {
tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t)); if (tf_ssb_connection_is_connected(connection) && !tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)) && !tf_ssb_connection_is_closing(connection))
*async = (tf_ssb_connection_send_history_stream_t) { {
.request_number = request_number, tf_ssb_connection_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t));
.sequence = sequence, *async = (tf_ssb_connection_send_history_stream_t) {
.keys = keys, .request_number = request_number,
.live = live, .sequence = sequence,
.end_request = end_request, .keys = keys,
}; .live = live,
snprintf(async->author, sizeof(async->author), "%s", author); .end_request = end_request,
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(
@ -976,194 +966,20 @@ static void _tf_ssb_rpc_createHistoryStream(
JS_FreeValue(context, arg_array); JS_FreeValue(context, arg_array);
} }
typedef struct _ebt_clock_row_t static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connection)
{ {
char id[k_id_base64_len]; tf_ssb_ebt_t* ebt = tf_ssb_connection_get_ebt(connection);
int64_t value; tf_ssb_ebt_clock_t* clock = tf_ssb_ebt_get_messages_to_send(ebt);
} ebt_clock_row_t; if (clock)
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++)
{ {
int64_t sequence = 0; for (int i = 0; i < clock->count; i++)
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))
{ {
int64_t sequence = 0; tf_ssb_ebt_clock_entry_t* entry = &clock->entries[i];
tf_ssb_db_get_latest_message_by_author(ssb, id, &sequence, NULL, 0); int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection);
JS_SetPropertyStr(context, full_clock, id, JS_NewInt64(context, sequence == -1 ? -1 : (sequence << 1))); 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, entry->id, entry->value, false, live, false);
} }
JS_FreeValue(context, in_clock); tf_free(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);
} }
} }
@ -1177,14 +993,54 @@ 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_replicate_resend_clock(tf_ssb_connection_t* connection, void* user_data) static void _tf_ssb_rpc_ebt_send_clock_callback(const tf_ssb_ebt_clock_t* clock, int32_t request_number, void* user_data)
{ {
resend_clock_t* resend = user_data; resend_clock_t* resend = user_data;
_tf_ssb_rpc_ebt_replicate_send_clock(resend->connection, resend->request_number, JS_UNDEFINED); tf_ssb_connection_t* connection = resend->connection;
tf_ssb_connection_set_sent_clock(resend->connection, true);
tf_free(user_data); if (clock && clock->count)
{
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)
@ -1207,33 +1063,38 @@ 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);
if (tf_ssb_connection_get_sent_clock(connection)) resend_clock = !tf_ssb_is_shutting_down(ssb) && !tf_ssb_connection_is_closing(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, _tf_ssb_rpc_ebt_replicate_resend_clock, resend); tf_ssb_connection_schedule_idle(connection, "ebt.clock", _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);
} }
@ -1400,14 +1261,17 @@ 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 = _get_global_setting_int64(ssb, "blob_expire_age_seconds", -1); int64_t age = -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();
sqlite3* db = tf_ssb_acquire_db_writer(ssb); 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;
@ -1469,12 +1333,18 @@ 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;
if (!_get_global_setting_bool(ssb, "delete_stale_feeds", false)) sqlite3* db = tf_ssb_acquire_db_reader(ssb);
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();
int replication_hops = (int)_get_global_setting_int64(ssb, "replication_hops", 2); int64_t 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 };
@ -1493,7 +1363,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);
sqlite3* db = tf_ssb_acquire_db_writer(ssb); 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); tf_ssb_connection_close(tun0, "done");
uv_run(&loop, UV_RUN_NOWAIT); uv_run(&loop, UV_RUN_NOWAIT);

View File

@ -17,6 +17,7 @@
#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"
@ -701,6 +702,7 @@ 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);
@ -932,21 +934,6 @@ 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,14 +310,6 @@ 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,7 +52,8 @@ 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];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -65,7 +66,8 @@ 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];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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);
@ -98,7 +100,8 @@ static void _test_sandbox(const tf_test_options_t* options)
"exit(r);\n"); "exit(r);\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -130,7 +133,8 @@ static void _test_child(const tf_test_options_t* options)
"exit(0);\n"); "exit(0);\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -168,7 +172,8 @@ static void _test_promise(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -210,7 +215,8 @@ static void _test_promise_remote_throw(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -254,7 +260,8 @@ static void _test_promise_remote_reject(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -331,7 +338,8 @@ static void _test_this(const tf_test_options_t* options)
"exit(0);\n"); "exit(0);\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -361,7 +369,8 @@ static void _test_await(const tf_test_options_t* options)
"\n"); "\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -391,14 +400,16 @@ static void _test_import(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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);
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/bad.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -416,7 +427,8 @@ 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];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -434,7 +446,8 @@ 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];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -482,7 +495,8 @@ static void _test_uint8array(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -523,7 +537,8 @@ static void _test_float(const tf_test_options_t* options)
"print(\"child ready\");\n"); "print(\"child ready\");\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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;
@ -610,7 +625,8 @@ static void _test_socket(const tf_test_options_t* options)
"});\n"); "});\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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));
@ -659,7 +675,8 @@ static void _test_b64(const tf_test_options_t* options)
"}\n"); "}\n");
char command[256]; char command[256];
snprintf(command, sizeof(command), "%s run --db-path=:memory: -s out/test.js" TEST_ARGS, options->exe_path); unlink("out/test_db0.sqlite");
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/apps") }, { .name = "index", .type = "string", .description = "Default path.", .default_value = JS_NewString(context, "/~core/ssb/") },
{ .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 visible from local accounts and related follows.", .description = "Periodically delete feeds that aren't visible from local accounts or related follows.",
.default_value = JS_FALSE }, .default_value = JS_FALSE },
}; };

View File

@ -150,6 +150,19 @@ 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.26" #define VERSION_NUMBER "0.0.27-wip"
#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') driver.get('http://localhost:8888/~core/apps/')
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') driver.get('http://localhost:8888/~core/apps/')
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') driver.get('http://localhost:8888/~core/apps/')
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') driver.get('http://localhost:8888/~core/apps/')
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,8 +73,10 @@ build_the_thing() {
no-whirlpool no-whirlpool
no-weak-ssl-ciphers no-weak-ssl-ciphers
no-zlib no-zlib
-Os -Oz
-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 -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined -miphoneos-version-min=9.0" OPTIONS="--static -static -Os -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 -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined" OPTIONS="--static -static -Os -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

94
tools/ssl-local Executable file
View File

@ -0,0 +1,94 @@
#!/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