142 Commits

Author SHA1 Message Date
9614d03bef ssb: Fix a timer leak I observed trying to wrap up 0.0.24. 2024-10-30 19:32:24 -04:00
32a335c676 test: Retry harder. 2024-10-30 19:32:05 -04:00
06e27fc1e0 docs: Update the 0.0.24 changelog. 2024-10-30 19:31:33 -04:00
1f40e8dcd9 build: Let's build 0.0.24. 2024-10-30 12:56:20 -04:00
77ff8cef1f db: Fix the db app, and show a message instead of an ugly error when you're not signed in. 2024-10-29 20:19:50 -04:00
ef844fbccb build: Oh, you can generate a .flatpak file, if that's your thing. 2024-10-27 18:50:07 -04:00
070dc5a4c0 build: flatpak filesystem access tweaks. 2024-10-27 14:37:37 -04:00
177ef1cdcc build: A flatpak experiment. I still don't get it. 2024-10-27 14:31:11 -04:00
4b1ebf02e1 js: Remove a stale /save reference, and exercise re-saving an app in the test. 2024-10-27 14:05:20 -04:00
863e50203e js: Move /save to C. 2024-10-27 13:42:56 -04:00
01b8c209de core: Testing a theory to encourage clean shutdowns. 2024-10-25 15:34:43 -04:00
30e92f2bc1 js: Fix typo in /delete. 2024-10-25 15:34:01 -04:00
02accabb4a js: Oh yeah, administrators can delete core apps still. 2024-10-25 15:20:54 -04:00
fa00a41fe0 js: Move app delete to C. 2024-10-25 13:58:06 -04:00
2e66666bdf ssb: Indicate which connections are one-shot / sync now connections. 2024-10-25 12:53:45 -04:00
4fe3c9a751 test: Verify that deleting apps actually does something. 2024-10-25 12:34:22 -04:00
0a35e14590 js: Fix database.getall(). 2024-10-23 21:50:34 -04:00
e979c176e3 test: Exercise nominally creating and deleting an app. 2024-10-23 18:48:42 -04:00
a0d9c3dc29 js: Move the global 404 response to C. 2024-10-23 18:27:36 -04:00
efcb68351c docs: Reminding myself how to make a release. 2024-10-23 18:06:56 -04:00
94e8bf2e1c test: Add some nominal testing for the new publish command. 2024-10-23 15:57:44 -04:00
82d1a294a6 ssb: prettier. 2024-10-23 15:38:49 -04:00
de20274589 ssb: Add a publish command that can be used to publish messages from the command-line. 2024-10-23 15:38:07 -04:00
2f193e64c8 ssb: Show muxrpc command names when possible in verbose logging. 2024-10-23 15:37:28 -04:00
86751362cb ssb: Indicate which muxrpc sends failed, and use that to fix some replication nonsense and log noise. 2024-10-23 14:13:55 -04:00
4118323631 build: Fix some cflags not making it to libsodium. Disable the libsodium -flto warning a more different way. 2024-10-23 12:02:41 -04:00
0d134f7f10 update: CodeMirror to latest. 2024-10-23 11:34:30 -04:00
409724cfcd update: OpenSSL 3.4.0. 2024-10-23 11:32:45 -04:00
799a33be40 update: libbacktrace to latest. 2024-10-23 11:20:23 -04:00
2fa9c66495 update: libuv 1.49.2. 2024-10-23 11:05:15 -04:00
ad818a8e7e update: sqlite 3.47.0. 2024-10-23 11:01:42 -04:00
581f72b3f8 ssb: Disallow rich text paste on Firefox. Didn't realize it doesn't support contenteditable='plaintext-only'. 2024-10-17 12:41:50 -04:00
1dd7e4347c js: Kill setGlobalSettings. 2024-10-16 21:02:48 -04:00
36cc9398c7 js: Move storePermission to C. 2024-10-16 20:36:53 -04:00
68817feeec js: Kill getSessionProcessBlob. 2024-10-16 19:50:31 -04:00
97661e2ca2 http: Fix some headers. 2024-10-16 19:26:26 -04:00
72def5ae6d js: Move /view to C. 2024-10-16 19:16:45 -04:00
e638b155a1 js: Kill gGlobalSettings. Decouples things a bit. 2024-10-16 18:11:08 -04:00
32db18b0d6 ssb: Close the reactions list dialog by clicking off of it. 2024-10-16 12:35:10 -04:00
b653a5250d build: Appease gcc 14. 2024-10-15 12:41:47 -04:00
30329f7cad ssb: No duplicate connections, even with tunnels. This is confusing. 2024-10-14 12:44:21 -04:00
29a1478c86 ssb: No duplicate tunnels. 2024-10-13 18:13:31 -04:00
c882bf31ec docs: grammar 2024-10-13 14:58:21 -04:00
17ccb8f083 update: libuv 1.49.1. 2024-10-13 14:46:24 -04:00
0e7d2a8b0e ssb: The identity app now lets you switch out the server identity if you are an administrator. 2024-10-13 14:40:14 -04:00
3743543ef8 welcome: prettier 2024-10-13 14:19:55 -04:00
700dd7b45a build: Appease OpenBSD. 2024-10-11 19:04:49 -04:00
c2eb73fd8a update: c-ares 1.34.1. 2024-10-11 18:25:09 -04:00
e1f4f7f95b ssb: Don't stretch profile images when fitting into a circle. I swear I did this ages ago. 2024-10-10 21:41:34 -04:00
37401409c6 ios: Include data in the app. How did this ever work? 2024-10-10 21:32:35 -04:00
b282631cd5 ios: Mobile provision junk. 2024-10-10 20:43:13 -04:00
9618d3b3f3 welcome: Link to the open testing Google Play build. 2024-10-09 12:20:37 -04:00
c9f997d121 update: commonmark 0.31.2. 2024-10-08 20:40:29 -04:00
f1dee2a089 ssb: Why would I not log the host of failed DNS requests? 2024-10-08 20:16:04 -04:00
8273277c91 editor: Fix in-browser prettification of html files. 2024-10-08 20:15:04 -04:00
9758844da3 editor: Fix visible whitespace toggle. 2024-10-08 19:56:58 -04:00
e41c7fbbc7 welcome: prettier 2024-10-08 19:41:55 -04:00
24db8a5a49 welcome: Slight wording updates. 2024-10-08 19:28:23 -04:00
36e82b9873 ssb: Sync now connects to room members one level deep. 2024-10-08 19:10:33 -04:00
8a32f2b8b1 Latest CodeMirror. 2024-10-08 12:44:49 -04:00
277830bc3c format: Sort includes. Yes, please. 2024-10-08 12:19:44 -04:00
a8fa969114 Lit 3.2.1. 2024-10-08 12:18:37 -04:00
c3f3dced68 docs: I figured out how to disable the gitea release zips lacking submodules (DISABLE_DOWNLOAD_SOURCE_ARCHIVES), so remove the caveat about them from the docs. 2024-10-06 12:16:30 -04:00
85fce59c0c ssb: Sync on demand fixes. Avoid keeping message streams live in this mode. 2024-10-06 11:50:49 -04:00
8a6147d512 ssb: Beginnings of a "sync now" mode for mobile. 2024-10-06 11:14:37 -04:00
e799b256b2 ssb: Even more muxrpc activity status fixes. 2024-10-05 21:00:50 -04:00
b222dc0ca8 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-10-05 20:44:17 -04:00
c52c6b04ca ssb: muxrpc activity status fixes. 2024-10-05 20:44:01 -04:00
b95eed46bb ssb: Fix a request leak in tunnel.connect. 2024-10-04 22:05:17 -04:00
7c36a543da ssb: Fix a leaked request and a shutdown error. 2024-10-04 12:39:39 -04:00
90e000c18e ssb: Fix activity indication of muxrpc requests expiring. 2024-10-03 12:41:45 -04:00
1bb9d737d8 ssb: Show activity for each muxrpc request. 2024-10-02 20:43:51 -04:00
9a5db2ec51 appimage: Put update information in the appimage. 2024-10-02 20:03:44 -04:00
dbed29a044 Update prettier. And run it some more. 2024-10-02 18:49:17 -04:00
681859531c muxrpc: Simplifying comparing RPC names. This has just always bugged me. 2024-10-02 18:46:12 -04:00
8e1ad6b16a js: Unused external. 2024-10-02 18:12:28 -04:00
5448f1ba2d Update CodeMirror. 2024-10-02 18:00:40 -04:00
e43da4e1a3 welcome: Better OpenSSL link. 2024-10-02 17:56:36 -04:00
eaa9da49cc welcome: F-Droid/AppImage links. 2024-10-02 17:55:01 -04:00
40873b529c build: Suppress a warning in libuv on arm. 2024-09-30 12:37:41 -04:00
8cc4c19d73 Prettier and generated files I've missed. 2024-09-30 12:15:27 -04:00
bb9c18faf1 Some missing log newlines. 2024-09-30 12:13:57 -04:00
fabdfb76b9 android: readParcelable compatibility. 2024-09-29 08:18:46 -04:00
bce263a928 android: Use FileObserver, which is actually compatible with api level 24 which we claim to support. 2024-09-29 00:17:38 -04:00
195920e476 android: Avoid a ClosedWatchServiceException. 2024-09-28 23:25:28 -04:00
a821d895c5 docs: Give working advice on how to get the tree and dependencies. 2024-09-28 07:11:47 -04:00
ab1b6ec27d build: Add a dependency off appimagetool?? 2024-09-27 22:10:01 -04:00
6dc099809f build: This creates a working AppImage. 2024-09-27 21:19:18 -04:00
03c8b75994 Let's start work on 0.0.24. 2024-09-25 20:26:57 -04:00
38887452ad nix => 0.0.23. 2024-09-25 20:20:14 -04:00
7512edad59 build: I forgot to build the .xz as part of dist. 2024-09-25 20:02:45 -04:00
944c895bcd Generated 0.0.23 files. Oops. 2024-09-25 19:55:12 -04:00
e7d87ee8e2 I think that worked. Let's build 0.0.23. 2024-09-25 19:49:52 -04:00
cfdbd10635 ci: Oh, we can not require fuse? 2024-09-25 19:45:05 -04:00
d3a2d8733f ci: Maybe we don't need to manually load it? 2024-09-25 19:37:17 -04:00
a7e623d817 ci: apt install fuse3? 2024-09-25 19:35:47 -04:00
3f0c37cea4 ci: modprobe harder. 2024-09-25 19:33:30 -04:00
2c96a6d22a ci: modprobe fuse? Disable things to get this going faster until it works. 2024-09-25 19:30:33 -04:00
57b4214a72 AppImages require FUSE to run. 2024-09-25 19:10:13 -04:00
433b3d39d9 ci: Build the appimage, for real.nofoolin. 2024-09-25 18:50:06 -04:00
26441ed45c Let's try to artifact the appimage. 2024-09-25 12:49:32 -04:00
92cd38c2a0 Change notes. 2024-09-25 12:33:06 -04:00
3b5a06794f libuv 1.49.0. 2024-09-25 12:11:17 -04:00
d104409272 Prevent votes from overflowing. 2024-09-23 12:43:48 -04:00
e5f58c2898 Produce user info for the server identity for admin users. 2024-09-19 12:22:38 -04:00
f83863ef01 This doc was ancient, so paste some of the latest from the wiki in. It's something. 2024-09-18 20:16:35 -04:00
837f069cf5 Update CodeMirror. 2024-09-18 20:16:35 -04:00
9f057dc29a Add F-Droid and c-ares to the welcome page. 2024-09-18 20:01:36 -04:00
c4904f176c Distinguish the server identity in the identity app. 2024-09-18 19:09:44 -04:00
d3a5aba703 A brave new world where admin users can use the server identity. 2024-09-17 12:47:28 -04:00
9e283e427c Fix viewing apps by blob ID URL. 2024-09-16 12:45:06 -04:00
133ba31d66 c-ares 1.33.1. 2024-09-15 08:57:22 -04:00
241a65a92a sus. Disable a warning in c-ares during ltcg with gcc 13 on Haiku. 2024-09-15 12:52:28 -04:00
0b54795bab Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-09-11 20:28:06 -04:00
6208193de5 Fix plumbing for replies on the search tab. 2024-09-11 20:25:55 -04:00
c53321532f Update CodeMirror. 2024-09-11 20:18:57 -04:00
34f25e3e06 How did I not have an index on type? Wow. 2024-09-11 19:53:07 -04:00
c46244366e I don't know what GHES is, but it says to use this version, so sure. 2024-09-11 19:52:43 -04:00
6518af04fc It finally built. Now let's upload some artifacts. 2024-09-11 19:33:37 -04:00
bf137ff1f7 Seriously? More volume mount config. 2024-09-11 19:00:30 -04:00
1877955b62 Syntax? 2024-09-11 18:54:39 -04:00
50d0875de2 Mount volumes across another container? 2024-09-11 18:49:06 -04:00
bf151e6b7d I am confused. 2024-09-11 18:41:55 -04:00
82893402d0 Maybe we can sign here? 2024-09-10 21:54:36 -04:00
8049102787 More silent OpenSSL build for mingw. 2024-09-10 21:19:15 -04:00
f42cc3d9fd Find the Android SDKs you just installed. 2024-09-10 21:01:20 -04:00
5f9a5208db Appease the sdkmanager version number whatever. 2024-09-10 20:34:25 -04:00
6df506d238 Spelling. 2024-09-10 20:25:51 -04:00
2bd3354256 yaml better?? 2024-09-10 20:22:06 -04:00
b55aaa1d18 Maybe CI android?? 2024-09-10 20:20:40 -04:00
34e19505bd No longer need this test. 2024-09-09 15:57:43 -04:00
6e06ec0904 Clean up connections that don't handshake in time. 2024-09-09 15:25:10 -04:00
a5814074fe OpenSSL 3.3.2. 2024-09-04 20:24:32 -04:00
d7479df5a2 Latest CodeMirror. 2024-09-04 20:11:28 -04:00
34508aa0ae dist slightly more in parallel. Exclude dotfiles from data.zip. 2024-09-04 20:07:26 -04:00
ae096b2c9c Try harder to make webview localStorage work on different versions. I suspect that's what is about. 2024-09-04 12:50:12 -04:00
95d036e34a Build an AppImage. Why not? 2024-08-28 20:55:52 -04:00
4af5e8ec42 I guess prettier says this, now. 2024-08-28 20:24:26 -04:00
2a5f71bd5d This now lives in the fdroiddata repository. 2024-08-28 19:59:49 -04:00
97fb63dda1 Actually 0.0.23-wip. 2024-08-28 19:59:34 -04:00
87d42e3b3b 0.0.23-wip again. Let's gooooo. 2024-08-28 19:49:36 -04:00
0394129a4c nix => 0.0.22 again. 2024-08-28 19:42:31 -04:00
96 changed files with 12751 additions and 6085 deletions

@ -14,7 +14,7 @@ IndentWidth: 4
MaxEmptyLinesToKeep: 1
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false
SortIncludes: false
SortIncludes: true
TabWidth: 4
UseTab: Always
...

@ -1,40 +0,0 @@
Categories:
- Internet
License: MIT
AutoName: tildefriends
AuthorName: Cory McWilliams
AuthorEmail: cory@tildefriends.net
RepoType: git
Repo: https://dev.tildefriends.net/cory/tildefriends.git
Builds:
- versionName: 0.0.21-wip
versionCode: 22
commit: 09b6a00731d45fa160b23a2c44be6def98d92d6a
subdir: src/android
submodules: true
sudo:
- apt-get update
- apt-get install -y ant make zip
androidupdate:
- no
scandelete:
- deps/libuv/docs/src/static/diagrams.key/Index.zip
- deps/openssl_src/cloudflare-quiche/*
- deps/openssl_src/fuzz/*
- deps/openssl_src/gost-engine/*
- deps/openssl_src/test/*
- deps/openssl_src/tlslite-ng/*
prebuild:
- sdkmanager "platforms;android-34" "build-tools;34.0.0"
build:
- mkdir bin/
- ANDROID_SDK=$$SDK$$ ANDROID_NDK=$$NDK$$ ANDROID_NDK_ROOT=$$NDK$$ make -C ../../ -j`nproc` fdroid
ndk: r26d
AutoUpdateMode: Version ^v[0-9\.]+$
UpdateCheckMode: Tags
CurrentVersion: 0.0.21-wip
CurrentVersionCode: 22

@ -5,11 +5,34 @@ on: [push]
jobs:
Build-All:
runs-on: ubuntu-latest
container:
valid_volumes: ['/opt/keys']
volumes:
- /opt/keys:/opt/keys
steps:
- name: check out code
uses: actions/checkout@v4
with:
submodules: true
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64
- run: make all -j`nproc` docs
- run: ln -s /opt/keys .keys
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
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: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
- run: docker build .
- uses: actions/upload-artifact@v3
with:
path: out/TildeFriends-release.fdroid.apk
- uses: actions/upload-artifact@v3
with:
path: out/winrelease/tildefriends.exe
- uses: actions/upload-artifact@v3
with:
path: out/tildefriends-x86_64.AppImage

1
.gitignore vendored

@ -4,6 +4,7 @@ db.*
deps/ios_toolchain/
deps/openssl/
dist/
.flatpak-builder
.keys
logs/
**/node_modules

@ -3,12 +3,14 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 26
VERSION_NUMBER := 0.0.22
VERSION_NAME := Get born soon.
VERSION_CODE := 29
VERSION_NUMBER := 0.0.24
VERSION_NAME := Honey bunches of boats.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
PROJECT = tildefriends
BUILD_DIR ?= out
@ -35,7 +37,8 @@ BUILD_TYPES := debug release
CFLAGS += -Dstatic_assert=_Static_assert
LDFLAGS += \
-lbsd \
-lnetwork
-lnetwork \
-Wno-stringop-overflow
else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release
CFLAGS += \
@ -63,6 +66,7 @@ CFLAGS += \
-g
LDFLAGS += \
-Wno-attributes \
-Wno-aggressive-loop-optimizations \
-flto=auto
ANDROID_MIN_SDK_VERSION := 24
@ -159,7 +163,9 @@ NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TA
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
ifneq ($(UNAME_S),OpenBSD)
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
endif
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
@ -219,12 +225,15 @@ $(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
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux)
all: appimage
endif
ifneq ($(UNAME_S),Haiku)
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
@ -272,98 +281,101 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
endif
ARES_SOURCES := \
deps/c-ares/src/lib/ares_platform.c \
deps/c-ares/src/lib/record/ares_dns_mapping.c \
deps/c-ares/src/lib/record/ares_dns_parse.c \
deps/c-ares/src/lib/record/ares_dns_write.c \
deps/c-ares/src/lib/record/ares_dns_name.c \
deps/c-ares/src/lib/record/ares_dns_record.c \
deps/c-ares/src/lib/record/ares_dns_multistring.c \
deps/c-ares/src/lib/ares_destroy.c \
deps/c-ares/src/lib/ares_data.c \
deps/c-ares/src/lib/ares_sysconfig.c \
deps/c-ares/src/lib/ares_addrinfo2hostent.c \
deps/c-ares/src/lib/ares_addrinfo_localhost.c \
deps/c-ares/src/lib/ares_android.c \
deps/c-ares/src/lib/ares_cancel.c \
deps/c-ares/src/lib/ares_metrics.c \
deps/c-ares/src/lib/ares_getnameinfo.c \
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
deps/c-ares/src/lib/legacy/ares_create_query.c \
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
deps/c-ares/src/lib/legacy/ares_expand_name.c \
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
deps/c-ares/src/lib/legacy/ares_expand_string.c \
deps/c-ares/src/lib/legacy/ares_fds.c \
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
deps/c-ares/src/lib/legacy/ares_getsock.c \
deps/c-ares/src/lib/windows_port.c \
deps/c-ares/src/lib/ares_qcache.c \
deps/c-ares/src/lib/ares_update_servers.c \
deps/c-ares/src/lib/ares_process.c \
deps/c-ares/src/lib/ares_close_sockets.c \
deps/c-ares/src/lib/ares_conn.c \
deps/c-ares/src/lib/ares_cookie.c \
deps/c-ares/src/lib/ares_data.c \
deps/c-ares/src/lib/ares_destroy.c \
deps/c-ares/src/lib/ares_free_hostent.c \
deps/c-ares/src/lib/ares_free_string.c \
deps/c-ares/src/lib/ares_freeaddrinfo.c \
deps/c-ares/src/lib/ares_getaddrinfo.c \
deps/c-ares/src/lib/ares_getenv.c \
deps/c-ares/src/lib/ares_gethostbyaddr.c \
deps/c-ares/src/lib/ares_gethostbyname.c \
deps/c-ares/src/lib/ares_getnameinfo.c \
deps/c-ares/src/lib/ares_hosts_file.c \
deps/c-ares/src/lib/ares_init.c \
deps/c-ares/src/lib/ares_library_init.c \
deps/c-ares/src/lib/ares_metrics.c \
deps/c-ares/src/lib/ares_options.c \
deps/c-ares/src/lib/ares_parse_into_addrinfo.c \
deps/c-ares/src/lib/ares_process.c \
deps/c-ares/src/lib/ares_qcache.c \
deps/c-ares/src/lib/ares_query.c \
deps/c-ares/src/lib/ares_search.c \
deps/c-ares/src/lib/ares_send.c \
deps/c-ares/src/lib/dsa/ares__slist.c \
deps/c-ares/src/lib/dsa/ares__htable.c \
deps/c-ares/src/lib/dsa/ares__llist.c \
deps/c-ares/src/lib/dsa/ares__htable_szvp.c \
deps/c-ares/src/lib/dsa/ares__htable_asvp.c \
deps/c-ares/src/lib/dsa/ares__htable_vpvp.c \
deps/c-ares/src/lib/dsa/ares__htable_strvp.c \
deps/c-ares/src/lib/dsa/ares__array.c \
deps/c-ares/src/lib/ares__socket.c \
deps/c-ares/src/lib/event/ares_event_poll.c \
deps/c-ares/src/lib/event/ares_event_thread.c \
deps/c-ares/src/lib/event/ares_event_select.c \
deps/c-ares/src/lib/event/ares_event_kqueue.c \
deps/c-ares/src/lib/ares_set_socket_functions.c \
deps/c-ares/src/lib/ares_socket.c \
deps/c-ares/src/lib/ares_sortaddrinfo.c \
deps/c-ares/src/lib/ares_strerror.c \
deps/c-ares/src/lib/ares_sysconfig.c \
deps/c-ares/src/lib/ares_sysconfig_files.c \
deps/c-ares/src/lib/ares_sysconfig_mac.c \
deps/c-ares/src/lib/ares_sysconfig_win.c \
deps/c-ares/src/lib/ares_update_servers.c \
deps/c-ares/src/lib/ares_version.c \
deps/c-ares/src/lib/dsa/ares_array.c \
deps/c-ares/src/lib/dsa/ares_htable.c \
deps/c-ares/src/lib/dsa/ares_htable_asvp.c \
deps/c-ares/src/lib/dsa/ares_htable_dict.c \
deps/c-ares/src/lib/dsa/ares_htable_strvp.c \
deps/c-ares/src/lib/dsa/ares_htable_szvp.c \
deps/c-ares/src/lib/dsa/ares_htable_vpvp.c \
deps/c-ares/src/lib/dsa/ares_llist.c \
deps/c-ares/src/lib/dsa/ares_slist.c \
deps/c-ares/src/lib/event/ares_event_configchg.c \
deps/c-ares/src/lib/event/ares_event_epoll.c \
deps/c-ares/src/lib/event/ares_event_kqueue.c \
deps/c-ares/src/lib/event/ares_event_poll.c \
deps/c-ares/src/lib/event/ares_event_select.c \
deps/c-ares/src/lib/event/ares_event_thread.c \
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
deps/c-ares/src/lib/event/ares_event_win32.c \
deps/c-ares/src/lib/ares_search.c \
deps/c-ares/src/lib/ares__parse_into_addrinfo.c \
deps/c-ares/src/lib/ares__hosts_file.c \
deps/c-ares/src/lib/ares_getaddrinfo.c \
deps/c-ares/src/lib/ares__addrinfo2hostent.c \
deps/c-ares/src/lib/ares_freeaddrinfo.c \
deps/c-ares/src/lib/ares_strerror.c \
deps/c-ares/src/lib/ares_version.c \
deps/c-ares/src/lib/ares_gethostbyaddr.c \
deps/c-ares/src/lib/ares__addrinfo_localhost.c \
deps/c-ares/src/lib/ares_free_hostent.c \
deps/c-ares/src/lib/ares__close_sockets.c \
deps/c-ares/src/lib/ares_free_string.c \
deps/c-ares/src/lib/ares_init.c \
deps/c-ares/src/lib/ares_options.c \
deps/c-ares/src/lib/str/ares_strcasecmp.c \
deps/c-ares/src/lib/str/ares__buf.c \
deps/c-ares/src/lib/str/ares_strsplit.c \
deps/c-ares/src/lib/str/ares_str.c \
deps/c-ares/src/lib/ares_sysconfig_mac.c \
deps/c-ares/src/lib/ares__sortaddrinfo.c \
deps/c-ares/src/lib/ares_sysconfig_files.c \
deps/c-ares/src/lib/util/ares__iface_ips.c \
deps/c-ares/src/lib/util/ares__timeval.c \
deps/c-ares/src/lib/util/ares_math.c \
deps/c-ares/src/lib/util/ares_rand.c \
deps/c-ares/src/lib/util/ares__threads.c \
deps/c-ares/src/lib/ares_query.c \
deps/c-ares/src/lib/ares_cookie.c \
deps/c-ares/src/lib/inet_net_pton.c \
deps/c-ares/src/lib/inet_ntop.c \
deps/c-ares/src/lib/ares_library_init.c \
deps/c-ares/src/lib/ares_android.c \
deps/c-ares/src/lib/ares_sysconfig_win.c \
deps/c-ares/src/lib/legacy/ares_create_query.c \
deps/c-ares/src/lib/legacy/ares_expand_name.c \
deps/c-ares/src/lib/legacy/ares_expand_string.c \
deps/c-ares/src/lib/legacy/ares_fds.c \
deps/c-ares/src/lib/legacy/ares_getsock.c \
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
deps/c-ares/src/lib/record/ares_dns_mapping.c \
deps/c-ares/src/lib/record/ares_dns_multistring.c \
deps/c-ares/src/lib/record/ares_dns_name.c \
deps/c-ares/src/lib/record/ares_dns_parse.c \
deps/c-ares/src/lib/record/ares_dns_record.c \
deps/c-ares/src/lib/record/ares_dns_write.c \
deps/c-ares/src/lib/str/ares_buf.c \
deps/c-ares/src/lib/str/ares_str.c \
deps/c-ares/src/lib/str/ares_strsplit.c \
deps/c-ares/src/lib/util/ares_iface_ips.c \
deps/c-ares/src/lib/util/ares_math.c \
deps/c-ares/src/lib/util/ares_rand.c \
deps/c-ares/src/lib/util/ares_threads.c \
deps/c-ares/src/lib/util/ares_timeval.c \
deps/c-ares/src/lib/util/ares_uri.c \
deps/c-ares/src/lib/windows_port.c \
deps/c-ares/src/lib/ares_timeout.c
ARES_OBJS := $(call get_objs,ARES_SOURCES)
$(ARES_OBJS): CFLAGS += \
-Ideps/c-ares/include \
-Ideps/c-ares/src/lib \
-Ideps/c-ares/src/lib/include \
-Ideps/c-ares_config/ \
-D_GNU_SOURCE \
-Wno-unused-function \
@ -497,12 +509,17 @@ $(UV_OBJS): CFLAGS += \
-Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \
-Wno-sign-compare \
-Wno-unknown-attributes \
-Wno-unused-but-set-parameter \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable
-Wno-unused-variable \
-Wno-nonnull
$(UV_OBJS): CFLAGS += -fno-lto
$(filter out/win%,$(UV_OBJS)): CFLAGS += -Wno-cast-function-type
$(filter out/win%,$(UV_OBJS)): \
CFLAGS += \
-Wno-cast-function-type \
-Wno-missing-braces
ifeq ($(UNAME_S),Linux)
$(UV_OBJS): CFLAGS += \
-D_GNU_SOURCE
@ -578,7 +595,6 @@ ifneq ($(UNAME_S),OpenBSD)
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
-DHAVE_ALLOCA_H
endif
$(SODIUM_OBJS): CFLAGS := $(filter-out -flto,$(CFLAGS))
SQLITE_SOURCES := deps/sqlite/sqlite3.c
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
@ -842,7 +858,7 @@ PACKAGE_DIRS := \
deps/prettier/ \
deps/lit/
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f)))
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
@ -992,6 +1008,7 @@ out/data.zip: $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
@mkdir -p $(dir $@)
@cp -v $< $@
@cp -v out/data.zip $(@D)/
ifeq ($(HAVE_LINUX_IOS),1)
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
endif
@ -1066,14 +1083,41 @@ $(IOS_DEPS):
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends.AppDir
@mkdir -p out/tildefriends.AppDir/usr/bin
@mkdir -p out/tildefriends.AppDir/usr/share/applications
@mkdir -p out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/
@cp out/release/tildefriends out/tildefriends.AppDir/usr/bin/
@cp out/data.zip out/tildefriends.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends.AppDir/AppRun
@chmod +x out/tildefriends.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage
.PHONY: appimage
flatpak: out/
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
.PHONY: flatpak
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
tarball:
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf out/tildefriends-$(VERSION_NUMBER)
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
@mkdir -p out/tildefriends-$(VERSION_NUMBER)
@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
@tar \
--exclude=apps/welcome* \
@ -1090,9 +1134,15 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
-caf out/tildefriends-$(VERSION_NUMBER).tar.xz \
-C out/ \
tildefriends-$(VERSION_NUMBER)
.PHONY: tarball
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball
@mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
@ -1101,8 +1151,12 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
@echo "[cp] 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"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
.PHONY: dist
dist-test: dist

@ -19,8 +19,24 @@ Scuttlebutt, as well as a platform for writing and running web applications.
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
are kept up to date in the tree.
Tilde Friends uses git submodules, so either:
```
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
```
or:
```
git clone https://dev.tildefriends.net/cory/tildefriends.git
cd tildefriends
git submodule update --init --recursive
```
The `.tar.xz` source releases are all-inclusive.
1. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
2. To build, run `make debug` or `make release`. An executable will be
generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have

@ -28,7 +28,10 @@ function global_settings_set(key, value) {
}
function title_case(name) {
return name.split('_').map(x => x.charAt(0).toUpperCase() + x.substring(1)).join(' ');
return name
.split('_')
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
.join(' ');
}
window.addEventListener('load', function () {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💽"
"emoji": "💽",
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
}

@ -51,6 +51,19 @@ async function key_list(db) {
app.setDocument(doc);
}
function load() {
if (core.user?.credentials?.session) {
database_list();
} else {
app.setDocument(`<!DOCTYPE html>
<html>
<body style="background: #888">
<h1>Must be signed in to examine databases.</h1>
</body>
</html>`);
}
}
core.register('message', async function (message) {
if (message.event == 'hashChange') {
let hash = message.hash.substring(1);
@ -62,9 +75,9 @@ core.register('message', async function (message) {
} else if (hash.length) {
key_list(await database(hash.split(':').slice(1).join(':')));
} else {
database_list();
load();
}
}
});
database_list();
load();

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
"previous": "&5kw/2PgcySwOYCmAkjHTR2xTkIx3i7UjQmtQ8MfgWw8=.sha256"
}

@ -1,5 +1,7 @@
import * as tfrpc from '/tfrpc.js';
const is_admin = core.user?.credentials?.permissions?.administration;
tfrpc.register(async function get_private_key(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
@ -15,9 +17,13 @@ tfrpc.register(async function delete_id(id) {
tfrpc.register(async function reload() {
await main();
});
tfrpc.register(async function make_server(id) {
return await ssb.swapWithServerIdentity(id);
});
async function main() {
let ids = await ssb.getIdentities();
let server_id = await ssb.getServerIdentity();
await app.setDocument(
`
<head>
@ -98,6 +104,16 @@ async function main() {
alert('Error deleting ID: ' + e);
}
}
handler.make_server = async function make_server(event) {
let id = event.srcElement.dataset.id;
try {
if (confirm('Are you sure you want to make "' + id + '" the server identity?\\n\\nFor it to take effect, you will need to both sign in again and restart Tilde Friends.')) {
await tfrpc.rpc.make_server(id);
}
} catch (e) {
alert('Error making server ID: ' + e);
}
}
</script>
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
<div class="w3-card-4 w3-margin">
@ -116,14 +132,15 @@ async function main() {
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
<ul class="w3-ul">` +
ids
(ids ?? [])
.map(
(
id
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
${id}
${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''}
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
</li>`
)
.join('\n') +

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&xsmsLytB3VvoHphiFHZGGEvrCfTEVGXrGwobGTIYFPQ=.sha256"
"previous": "&IH3DadMNF785idnMI/LuCpIJQxzvpg1PDp8BI7m1Nx0=.sha256"
}

@ -103,6 +103,9 @@ tfrpc.register(async function encrypt(id, recipients, content) {
tfrpc.register(async function getActiveIdentity() {
return await ssb.getActiveIdentity();
});
tfrpc.register(async function sync() {
return await ssb.sync();
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -180,6 +180,13 @@ class TfComposeElement extends LitElement {
break;
}
}
event.preventDefault();
document.execCommand(
'insertText',
false,
event.clipboardData.getData('text/plain')
);
}
async submit() {

@ -73,18 +73,23 @@ class TfMessageElement extends LitElement {
}
}
if (this.message?.votes?.length) {
return html`<div class="w3-button" @click=${this.show_reactions}>
${(this.message.votes || []).map(
(vote) => html`
<span
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
vote.timestamp
)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
return html` <div class="w3-container">
<div
class="w3-button w3-bar w3-padding-small"
@click=${this.show_reactions}
>
${(this.message.votes || []).map(
(vote) => html`
<span
class="w3-bar-item w3-padding-small"
title="${this.users[vote.author]?.name ??
vote.author} ${new Date(vote.timestamp)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>
</div>`;
}
}

@ -27,8 +27,12 @@ class TfReactionsModalElement extends LitElement {
? html` <div
class="w3-modal w3-animate-opacity"
style="display: block; box-sizing: border-box"
@click=${this.clear}
>
<div class="w3-modal-content w3-card-4 w3-theme-d1">
<div
class="w3-modal-content w3-card-4 w3-theme-d1"
onclick="event.stopPropagation()"
>
<div class="w3-container w3-padding">
<header class="w3-container">
<h2>Reactions</h2>

@ -129,6 +129,7 @@ class TfTabConnectionsElement extends LitElement {
>
Close
</button>
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined
? '🚇'
@ -136,7 +137,9 @@ class TfTabConnectionsElement extends LitElement {
<div>
${requests.map(
(x) => html`
<span class="w3-tag w3-small"
<span
class=${'w3-tag w3-small ' +
(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
<span
class="w3-badge w3-white"
@ -156,10 +159,20 @@ class TfTabConnectionsElement extends LitElement {
`;
}
refresh() {
tfrpc.rpc.sync();
}
render() {
let self = this;
return html`
<div class="w3-container" style="box-sizing: border-box">
<button
class="w3-button w3-theme-l3 w3-circle w3-ripple w3-large"
@click=${this.refresh}
>
🔃
</button>
<h2>New Connection</h2>
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
<button

@ -5,6 +5,7 @@ import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement {
static get properties() {
return {
drafts: {type: Object},
whoami: {type: String},
users: {type: Object},
following: {type: Array},
@ -22,6 +23,10 @@ class TfTabSearchElement extends LitElement {
this.users = {};
this.following = [];
this.expanded = {};
this.drafts = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
async search(query) {
@ -70,6 +75,18 @@ class TfTabSearchElement extends LitElement {
}
}
draft(event) {
let id = event.detail.id || '';
let previous = this.drafts[id];
if (event.detail.draft !== undefined) {
this.drafts[id] = event.detail.draft;
} else {
delete this.drafts[id];
}
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
@ -81,7 +98,7 @@ class TfTabSearchElement extends LitElement {
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
`;
}
}

@ -37,7 +37,7 @@ class TfUserElement extends LitElement {
if (image_link !== undefined) {
image = html`<img
class="w3-circle"
style="width: 2em; height: 2em; vertical-align: middle"
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
src="/${image_link}/view"
/>`;
}

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&W5aJp2DgOW5rQ0AOIC9Ut3DpsahPrO6PjkJ1PQbNRdM=.sha256"
"previous": "&7gFmLW5zSMhmxWWY1+jeRcHdullgujSqGJg94lVgr1k=.sha256"
}

78
apps/welcome/appimage.svg Normal file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48px" height="48px" id="svg3832" version="1.1" inkscape:version="0.47 r22583" sodipodi:docname="appimage-assistant_alt3.svg">
<defs id="defs3834">
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3308-4-6-931-761-0" id="linearGradient2975" gradientUnits="userSpaceOnUse" x1="24.3125" y1="22.96875" x2="24.3125" y2="41.03125"/>
<linearGradient id="linearGradient3308-4-6-931-761-0">
<stop id="stop2919-2" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
<stop id="stop2921-76" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient4222" id="linearGradient2979" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,0.3704967,-0.3617496,0,33.508315,6.1670925)" x1="7.6485429" y1="26.437023" x2="41.861729" y2="26.437023"/>
<linearGradient id="linearGradient4222">
<stop id="stop4224" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
<stop id="stop4226" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient3308-4-6-931-761" id="linearGradient2982" gradientUnits="userSpaceOnUse" gradientTransform="translate(0,0.9999987)" x1="23.99999" y1="4.999989" x2="23.99999" y2="43"/>
<linearGradient id="linearGradient3308-4-6-931-761">
<stop id="stop2919" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
<stop id="stop2921" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
</linearGradient>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3575" id="radialGradient2985" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,1.0262008,-1.6561124,9.4072203e-4,-56.097482,-45.332325)" cx="48.42384" cy="-48.027504" fx="48.42384" fy="-48.027504" r="38.212933"/>
<linearGradient id="linearGradient3575">
<stop id="stop3577" style="stop-color:#fafafa;stop-opacity:1" offset="0"/>
<stop id="stop3579" style="stop-color:#e6e6e6;stop-opacity:1" offset="1"/>
</linearGradient>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient3993" id="radialGradient2990" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,2.0478765,-2.7410544,-8.6412258e-8,47.161382,-8.837436)" cx="9.3330879" cy="8.4497671" fx="9.3330879" fy="8.4497671" r="19.99999"/>
<linearGradient id="linearGradient3993">
<stop offset="0" style="stop-color:#a3c0d0;stop-opacity:1" id="stop3995"/>
<stop offset="1" style="stop-color:#427da1;stop-opacity:1" id="stop4001"/>
</linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient2508" id="linearGradient2992" gradientUnits="userSpaceOnUse" gradientTransform="translate(0,0.9674382)" x1="14.048676" y1="44.137306" x2="14.048676" y2="4.0000005"/>
<linearGradient id="linearGradient2508">
<stop offset="0" style="stop-color:#2e4a5a;stop-opacity:1" id="stop2510"/>
<stop offset="1" style="stop-color:#6e8796;stop-opacity:1" id="stop2512"/>
</linearGradient>
<radialGradient cx="4.9929786" cy="43.5" r="2.5" fx="4.9929786" fy="43.5" id="radialGradient2873-966-168" xlink:href="#linearGradient3688-166-749" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"/>
<linearGradient id="linearGradient3688-166-749">
<stop id="stop2883" style="stop-color:#181818;stop-opacity:1" offset="0"/>
<stop id="stop2885" style="stop-color:#181818;stop-opacity:0" offset="1"/>
</linearGradient>
<radialGradient cx="4.9929786" cy="43.5" r="2.5" fx="4.9929786" fy="43.5" id="radialGradient2875-742-326" xlink:href="#linearGradient3688-464-309" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"/>
<linearGradient id="linearGradient3688-464-309">
<stop id="stop2889" style="stop-color:#181818;stop-opacity:1" offset="0"/>
<stop id="stop2891" style="stop-color:#181818;stop-opacity:0" offset="1"/>
</linearGradient>
<linearGradient x1="25.058096" y1="47.027729" x2="25.058096" y2="39.999443" id="linearGradient2877-634-617" xlink:href="#linearGradient3702-501-757" gradientUnits="userSpaceOnUse"/>
<linearGradient id="linearGradient3702-501-757">
<stop id="stop2895" style="stop-color:#181818;stop-opacity:0" offset="0"/>
<stop id="stop2897" style="stop-color:#181818;stop-opacity:1" offset="0.5"/>
<stop id="stop2899" style="stop-color:#181818;stop-opacity:0" offset="1"/>
</linearGradient>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="7" inkscape:cx="24" inkscape:cy="24" inkscape:current-layer="layer1" showgrid="true" inkscape:grid-bbox="true" inkscape:document-units="px" inkscape:window-width="603" inkscape:window-height="484" inkscape:window-x="417" inkscape:window-y="162" inkscape:window-maximized="0"/>
<metadata id="metadata3837">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" inkscape:label="Layer 1" inkscape:groupmode="layer">
<g style="display:inline" id="g2036" transform="matrix(1.1,0,0,0.4444449,-2.4000022,25.11107)">
<g style="opacity:0.4" id="g3712" transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)">
<rect style="fill:url(#radialGradient2873-966-168);fill-opacity:1;stroke:none" id="rect2801" y="40" x="38" height="7" width="5"/>
<rect style="fill:url(#radialGradient2875-742-326);fill-opacity:1;stroke:none" id="rect3696" transform="scale(-1,-1)" y="-47" x="-10" height="7" width="5"/>
<rect style="fill:url(#linearGradient2877-634-617);fill-opacity:1;stroke:none" id="rect3700" y="40" x="10" height="7.0000005" width="28"/>
</g>
</g>
<rect style="fill:url(#radialGradient2990);fill-opacity:1;stroke:url(#linearGradient2992);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" id="rect5505" y="5.4674392" x="4.5" ry="2.2322156" rx="2.2322156" height="39" width="39"/>
<path style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4294-1" d="m 21,6.9687498 a 2.0165107,2.0165107 0 0 0 -2.03125,2.03125 l 0,3.9687502 -1.15625,0 a 2.0165107,2.0165107 0 0 0 -1.5,3.375 l 5.0625,5.75 c -0.06312,0.110777 -0.178724,0.246032 -0.21875,0.34375 -0.195898,0.478256 -0.25,0.83653 -0.25,1.21875 l 0,0.125 L 20.8125,23.6875 C 20.534322,23.409323 20.213169,23.162739 19.71875,22.96875 19.47154,22.87176 19.185456,22.791748 18.75,22.8125 c -0.435456,0.02075 -1.054055,0.210302 -1.46875,0.625 L 15.75,24.96875 c -0.414689,0.414689 -0.604245,1.033294 -0.625,1.46875 -0.02075,0.435456 0.05925,0.721537 0.15625,0.96875 C 15.475241,27.900677 15.721817,28.221821 16,28.5 l 0.09375,0.09375 -0.125,0 c -0.382218,0 -0.740493,0.0541 -1.21875,0.25 -0.239128,0.09795 -0.538285,0.214988 -0.84375,0.53125 -0.305465,0.316262 -0.625,0.914788 -0.625,1.53125 l 0,2.1875 c 0,0.616465 0.319536,1.214989 0.625,1.53125 0.305464,0.316261 0.604622,0.433301 0.84375,0.53125 0.478256,0.195898 0.83653,0.25 1.21875,0.25 l 0.125,0 L 16,35.5 c -0.278175,0.278176 -0.52476,0.599329 -0.71875,1.09375 -0.09699,0.24721 -0.177003,0.533292 -0.15625,0.96875 0.02075,0.435458 0.210304,1.054058 0.625,1.46875 l 1.53125,1.53125 c 0.414691,0.414697 1.033292,0.604245 1.46875,0.625 0.435458,0.02076 0.721537,-0.05926 0.96875,-0.15625 0.494425,-0.19399 0.81557,-0.440568 1.09375,-0.71875 l 0.09375,-0.09375 0,0.125 c 0,0.38222 0.0541,0.740495 0.25,1.21875 0.09795,0.239127 0.214989,0.538285 0.53125,0.84375 0.316261,0.305465 0.914783,0.625 1.53125,0.625 l 2.1875,0 c 0.616466,0 1.214989,-0.319534 1.53125,-0.625 0.316261,-0.305466 0.433302,-0.604622 0.53125,-0.84375 0.195896,-0.478255 0.25,-0.836532 0.25,-1.21875 l 0,-0.125 0.09375,0.09375 c 0.278176,0.278175 0.599329,0.52476 1.09375,0.71875 0.24721,0.09699 0.533292,0.177003 0.96875,0.15625 0.435458,-0.02075 1.054058,-0.210304 1.46875,-0.625 L 32.875,39.03125 C 33.289697,38.616559 33.479245,37.997958 33.5,37.5625 33.52076,37.127042 33.44074,36.840963 33.34375,36.59375 33.14976,36.099325 32.903182,35.77818 32.625,35.5 l -0.09375,-0.09375 0.125,0 c 0.38222,0 0.740494,-0.0541 1.21875,-0.25 0.239128,-0.09795 0.538286,-0.214988 0.84375,-0.53125 0.305464,-0.316262 0.625,-0.914787 0.625,-1.53125 l 0,-2.1875 c 0,-0.61646 -0.319535,-1.214987 -0.625,-1.53125 -0.305465,-0.316263 -0.604621,-0.433301 -0.84375,-0.53125 -0.478257,-0.195898 -0.836532,-0.25 -1.21875,-0.25 l -0.125,0 L 32.625,28.5 c 0.278177,-0.278177 0.52476,-0.599329 0.71875,-1.09375 C 33.44074,27.15904 33.520753,26.872957 33.5,26.4375 33.47925,26.002043 33.289697,25.383443 32.875,24.96875 L 31.34375,23.4375 c -0.414688,-0.414694 -1.03329,-0.604245 -1.46875,-0.625 -0.43546,-0.02076 -0.721537,0.05925 -0.96875,0.15625 -0.494426,0.193991 -0.815572,0.44057 -1.09375,0.71875 l -0.09375,0.09375 0,-0.125 c 0,-0.382218 -0.0541,-0.740493 -0.25,-1.21875 -0.09112,-0.22245 -0.228127,-0.500183 -0.5,-0.78125 l 4.71875,-5.3125 a 2.0165107,2.0165107 0 0 0 -1.5,-3.375 l -1.15625,0 0,-3.9687502 A 2.0165107,2.0165107 0 0 0 27,6.9687498 l -6,0 z M 24.3125,31.25 c 0.427097,0 0.75,0.322904 0.75,0.75 0,0.427096 -0.322903,0.75 -0.75,0.75 -0.427094,0 -0.75,-0.322906 -0.75,-0.75 0,-0.427094 0.322906,-0.75 0.75,-0.75 z"/>
<path style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4294" d="m 20.90625,8.0312498 a 0.96385067,0.96385067 0 0 0 -0.875,0.96875 l 0,5.0312502 -2.21875,0 A 0.96385067,0.96385067 0 0 0 17.09375,15.625 l 5.78125,6.53125 c -0.158814,0.0616 -0.341836,0.0951 -0.4375,0.1875 -0.169161,0.163386 -0.252971,0.323419 -0.3125,0.46875 -0.119058,0.290663 -0.15625,0.566746 -0.15625,0.84375 l 0,1.65625 C 21.718163,25.40233 21.485871,25.509772 21.25,25.625 l -1.1875,-1.1875 c -0.199651,-0.19965 -0.421433,-0.352095 -0.71875,-0.46875 -0.148659,-0.05833 -0.329673,-0.104846 -0.5625,-0.09375 -0.232827,0.0111 -0.53583,0.09833 -0.75,0.3125 L 16.5,25.71875 c -0.214168,0.214168 -0.301403,0.517173 -0.3125,0.75 -0.0111,0.232827 0.03542,0.41384 0.09375,0.5625 0.116655,0.297321 0.269096,0.519099 0.46875,0.71875 l 1.1875,1.1875 c -0.115228,0.235871 -0.222668,0.468163 -0.3125,0.71875 l -1.65625,0 c -0.277003,0 -0.553087,0.03719 -0.84375,0.15625 -0.145332,0.05953 -0.305363,0.143338 -0.46875,0.3125 -0.163387,0.169162 -0.3125,0.46403 -0.3125,0.78125 l 0,2.1875 c 0,0.317221 0.149114,0.612089 0.3125,0.78125 0.163386,0.169161 0.323419,0.252971 0.46875,0.3125 0.290663,0.119058 0.566746,0.15625 0.84375,0.15625 l 1.65625,0 c 0.08983,0.250587 0.197272,0.482879 0.3125,0.71875 L 16.75,36.25 c -0.199649,0.19965 -0.352095,0.421432 -0.46875,0.71875 -0.05833,0.148659 -0.104846,0.329672 -0.09375,0.5625 0.0111,0.232828 0.09833,0.535831 0.3125,0.75 l 1.53125,1.53125 c 0.214168,0.214172 0.517172,0.301403 0.75,0.3125 0.232828,0.0111 0.41384,-0.03542 0.5625,-0.09375 0.29732,-0.116655 0.519098,-0.269096 0.71875,-0.46875 L 21.25,38.375 c 0.235871,0.115228 0.468164,0.222668 0.71875,0.3125 l 0,1.65625 c 0,0.277003 0.03719,0.553087 0.15625,0.84375 0.05953,0.145331 0.143339,0.305364 0.3125,0.46875 0.169161,0.163386 0.464028,0.3125 0.78125,0.3125 l 2.1875,0 c 0.317221,0 0.612089,-0.149113 0.78125,-0.3125 0.169161,-0.163387 0.252971,-0.323419 0.3125,-0.46875 0.119057,-0.290663 0.15625,-0.566748 0.15625,-0.84375 l 0,-1.65625 c 0.250586,-0.08983 0.482879,-0.197272 0.71875,-0.3125 l 1.1875,1.1875 c 0.19965,0.199649 0.421432,0.352095 0.71875,0.46875 0.148659,0.05833 0.329672,0.104846 0.5625,0.09375 0.232828,-0.0111 0.535831,-0.09833 0.75,-0.3125 L 32.125,38.28125 c 0.214172,-0.214168 0.301403,-0.517172 0.3125,-0.75 0.0111,-0.232828 -0.03542,-0.41384 -0.09375,-0.5625 C 32.227095,36.67143 32.074654,36.449652 31.875,36.25 L 30.6875,35.0625 C 30.802728,34.82663 30.910168,34.594337 31,34.34375 l 1.65625,0 c 0.277004,0 0.553087,-0.03719 0.84375,-0.15625 0.145332,-0.05953 0.305364,-0.143339 0.46875,-0.3125 0.163386,-0.169161 0.3125,-0.46403 0.3125,-0.78125 l 0,-2.1875 c 0,-0.317219 -0.149114,-0.612088 -0.3125,-0.78125 C 33.805364,29.955838 33.645332,29.872029 33.5,29.8125 33.209336,29.693442 32.933253,29.65625 32.65625,29.65625 l -1.65625,0 C 30.91017,29.405663 30.802728,29.17337 30.6875,28.9375 L 31.875,27.75 c 0.19965,-0.19965 0.352095,-0.421432 0.46875,-0.71875 0.05833,-0.148659 0.104846,-0.329672 0.09375,-0.5625 -0.0111,-0.232828 -0.09833,-0.535831 -0.3125,-0.75 L 30.59375,24.1875 c -0.214167,-0.21417 -0.517171,-0.301403 -0.75,-0.3125 -0.232829,-0.0111 -0.41384,0.03542 -0.5625,0.09375 -0.29732,0.116656 -0.519099,0.269097 -0.71875,0.46875 L 27.375,25.625 c -0.235871,-0.115228 -0.468163,-0.222668 -0.71875,-0.3125 l 0,-1.65625 c 0,-0.277003 -0.03719,-0.553087 -0.15625,-0.84375 -0.05953,-0.145332 -0.143338,-0.305363 -0.3125,-0.46875 -0.169162,-0.163387 -0.46403,-0.3125 -0.78125,-0.3125 l -0.15625,0 5.65625,-6.40625 A 0.96385067,0.96385067 0 0 0 30.1875,14.03125 l -2.21875,0 0,-5.0312502 A 0.96385067,0.96385067 0 0 0 27,8.0312498 l -6,0 a 0.96385067,0.96385067 0 0 0 -0.09375,0 z M 24.3125,30.1875 c 1.002113,0 1.8125,0.810388 1.8125,1.8125 0,1.002112 -0.810387,1.8125 -1.8125,1.8125 C 23.31039,33.8125 22.5,33.002111 22.5,32 c 0,-1.002111 0.81039,-1.8125 1.8125,-1.8125 z"/>
<path style="fill:url(#radialGradient2985);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2317" d="M 21,8.9999996 21,15 17.8125,15 24,22 30.1875,15 27,15 l 0,-6.0000004 -6,0 z M 23.21875,23 c -0.172892,0 -0.28125,0.294922 -0.28125,0.65625 l 0,2.28125 C 22.24145,26.095996 21.585954,26.379869 21,26.75 l -1.625,-1.625 c -0.255498,-0.255497 -0.533998,-0.372253 -0.65625,-0.25 l -1.53125,1.53125 c -0.122254,0.122254 -0.0055,0.400753 0.25,0.65625 l 1.625,1.625 c -0.37013,0.585953 -0.654003,1.24145 -0.8125,1.9375 l -2.28125,0 c -0.361328,0 -0.65625,0.108357 -0.65625,0.28125 l 0,2.1875 c 0,0.172892 0.294922,0.28125 0.65625,0.28125 l 2.28125,0 c 0.158497,0.69605 0.44237,1.351546 0.8125,1.9375 l -1.625,1.625 c -0.255497,0.255498 -0.372254,0.533997 -0.25,0.65625 l 1.53125,1.53125 c 0.122252,0.122254 0.400752,0.0055 0.65625,-0.25 L 21,37.25 c 0.585954,0.37013 1.24145,0.654002 1.9375,0.8125 l 0,2.28125 C 22.9375,40.705077 23.045858,41 23.21875,41 l 2.1875,0 c 0.172893,0 0.28125,-0.294924 0.28125,-0.65625 l 0,-2.28125 c 0.69605,-0.158498 1.351546,-0.44237 1.9375,-0.8125 l 1.625,1.625 c 0.255498,0.255497 0.533997,0.372254 0.65625,0.25 l 1.53125,-1.53125 c 0.122254,-0.122252 0.0055,-0.400752 -0.25,-0.65625 l -1.625,-1.625 c 0.370129,-0.585954 0.654003,-1.24145 0.8125,-1.9375 l 2.28125,0 c 0.361329,0 0.65625,-0.108358 0.65625,-0.28125 l 0,-2.1875 c 0,-0.172893 -0.294921,-0.28125 -0.65625,-0.28125 l -2.28125,0 c -0.158497,-0.69605 -0.442371,-1.351547 -0.8125,-1.9375 l 1.625,-1.625 c 0.255497,-0.255497 0.372254,-0.533997 0.25,-0.65625 L 29.90625,24.875 C 29.783997,24.752745 29.505498,24.8695 29.25,25.125 l -1.625,1.625 c -0.585954,-0.370131 -1.24145,-0.654004 -1.9375,-0.8125 l 0,-2.28125 C 25.6875,23.294922 25.579143,23 25.40625,23 l -2.1875,0 z m 1.09375,6.21875 c 1.528616,0 2.78125,1.252635 2.78125,2.78125 0,1.528615 -1.252634,2.78125 -2.78125,2.78125 -1.528614,0 -2.78125,-1.252635 -2.78125,-2.78125 0,-1.528615 1.252636,-2.78125 2.78125,-2.78125 z"/>
<rect style="opacity:0.4;fill:none;stroke:url(#linearGradient2982);stroke-width:0.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" id="rect6741" y="6.4999886" x="5.4999981" ry="1.365193" rx="1.365193" height="37.000011" width="36.999985"/>
<path style="fill:none;stroke:url(#linearGradient2979);stroke-width:0.99829447;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" id="path2777" d="M 28.926376,15.466668 24,21.177578 18.963089,15.5 21.5,15.5 l 0,-6.0000004 5,0 0,6.0000004 2.426376,-0.03333 z"/>
<path style="fill:none;stroke:url(#linearGradient2975);stroke-width:1;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4243" d="m 23.4375,23.46875 c -0.01166,0.05381 -0.03125,0.100205 -0.03125,0.1875 l 0,2.28125 a 0.48185467,0.48185467 0 0 1 -0.375,0.46875 c -0.638467,0.145384 -1.238423,0.407111 -1.78125,0.75 a 0.48185467,0.48185467 0 0 1 -0.59375,-0.0625 l -1.625,-1.625 C 18.9779,25.4154 18.9477,25.40242 18.90625,25.375 l -1.21875,1.21875 c 0.02742,0.04145 0.0404,0.07165 0.09375,0.125 l 1.625,1.625 a 0.48185467,0.48185467 0 0 1 0.0625,0.59375 c -0.342888,0.542826 -0.604615,1.142782 -0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.46875,0.375 l -2.28125,0 c -0.08729,0 -0.133695,0.01959 -0.1875,0.03125 l 0,1.75 c 0.05381,0.01166 0.100205,0.03125 0.1875,0.03125 l 2.28125,0 a 0.48185467,0.48185467 0 0 1 0.46875,0.375 c 0.145385,0.638468 0.407112,1.238423 0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.0625,0.59375 l -1.625,1.625 c -0.05335,0.05335 -0.06633,0.08355 -0.09375,0.125 l 1.21875,1.21875 c 0.04145,-0.02742 0.07165,-0.0404 0.125,-0.09375 l 1.625,-1.625 A 0.48185467,0.48185467 0 0 1 21.25,36.84375 c 0.542827,0.342888 1.142781,0.604614 1.78125,0.75 a 0.48185467,0.48185467 0 0 1 0.375,0.46875 l 0,2.28125 c 0,0.08729 0.01959,0.133695 0.03125,0.1875 l 1.75,0 c 0.01166,-0.0538 0.03125,-0.100206 0.03125,-0.1875 l 0,-2.28125 a 0.48185467,0.48185467 0 0 1 0.375,-0.46875 c 0.638469,-0.145386 1.238423,-0.407112 1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 0.59375,0.0625 l 1.625,1.625 c 0.05335,0.05335 0.08355,0.06633 0.125,0.09375 l 1.21875,-1.21875 c -0.02742,-0.04145 -0.0404,-0.07165 -0.09375,-0.125 l -1.625,-1.625 a 0.48185467,0.48185467 0 0 1 -0.0625,-0.59375 c 0.342888,-0.542828 0.604615,-1.142783 0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.46875,-0.375 l 2.28125,0 c 0.08729,0 0.133695,-0.01959 0.1875,-0.03125 l 0,-1.75 c -0.0538,-0.01166 -0.100204,-0.03125 -0.1875,-0.03125 l -2.28125,0 a 0.48185467,0.48185467 0 0 1 -0.46875,-0.375 c -0.145385,-0.638467 -0.407113,-1.238424 -0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.0625,-0.59375 l 1.625,-1.625 c 0.05335,-0.05335 0.06633,-0.08355 0.09375,-0.125 L 29.71875,25.375 c -0.04145,0.02742 -0.07165,0.0404 -0.125,0.09375 l -1.625,1.625 a 0.48185467,0.48185467 0 0 1 -0.59375,0.0625 c -0.542827,-0.342889 -1.142783,-0.604616 -1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 -0.375,-0.46875 l 0,-2.28125 c 0,-0.0873 -0.01959,-0.133695 -0.03125,-0.1875 l -1.75,0 z m 0.875,5.28125 c 1.791829,0 3.25,1.458172 3.25,3.25 0,1.791828 -1.458171,3.25 -3.25,3.25 -1.791827,0 -3.25,-1.458172 -3.25,-3.25 0,-1.791828 1.458173,-3.25 3.25,-3.25 z"/>
</g>
</svg>

After

(image error) Size: 19 KiB

75
apps/welcome/f-droid.svg Normal file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48" height="48" viewBox="0 0 48.000001 48.000001" id="svg4230" version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="fdroid-logo.svg">
<defs id="defs4232">
<linearGradient inkscape:collect="always" id="linearGradient5212">
<stop style="stop-color:#ffffff;stop-opacity:0.09803922" offset="0" id="stop5214"/>
<stop style="stop-color:#ffffff;stop-opacity:0" offset="1" id="stop5216"/>
</linearGradient>
<radialGradient inkscape:collect="always" xlink:href="#linearGradient5212" id="radialGradient5220" cx="-98.23381" cy="3.4695871" fx="-98.23381" fy="3.4695871" r="22.671185" gradientTransform="matrix(0,1.9747624,-2.117225,3.9784049e-8,8.677247,1199.588)" gradientUnits="userSpaceOnUse"/>
<filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter4175" x="-0.023846937" width="1.0476939" y="-0.02415504" height="1.0483101">
<feGaussianBlur inkscape:collect="always" stdDeviation="0.45053152" id="feGaussianBlur4177"/>
</filter>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="11.313708" inkscape:cx="6.4184057" inkscape:cy="25.737489" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" units="px" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="0" inkscape:window-y="34" inkscape:window-maximized="1" gridtolerance="10000"/>
<metadata id="metadata4235">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
<cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/"/>
</cc:Work>
<cc:License rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
<cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
</cc:License>
</rdf:RDF>
</metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-1004.3622)">
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.4;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4175);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.613462,1006.3488 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" id="path4192" inkscape:connector-curvature="0"/>
<g id="g5012">
<g id="g4179" transform="matrix(-1,0,0,1,47.999779,0)">
<path style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.5889342,1006.8622 4.25,5.5" id="path4181" inkscape:connector-curvature="0" sodipodi:nodetypes="cc"/>
<path sodipodi:nodetypes="cccccc" inkscape:connector-curvature="0" id="path4183" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
<path sodipodi:nodetypes="ccccc" inkscape:connector-curvature="0" id="path4185" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
<path sodipodi:nodetypes="cscccc" inkscape:connector-curvature="0" id="path4187" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
</g>
<g id="g4955">
<path sodipodi:nodetypes="cc" inkscape:connector-curvature="0" id="path4945" d="m 2.5889342,1006.8622 4.25,5.5" style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" id="path4947" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccc"/>
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" id="path4951" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccc"/>
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" id="path4925" inkscape:connector-curvature="0" sodipodi:nodetypes="cscccc"/>
</g>
<g transform="translate(42,0)" id="g4967">
<rect style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4144" width="38" height="13" x="-37" y="1010.3622" rx="3" ry="3"/>
<rect ry="3" rx="3" y="1013.3622" x="-37" height="10" width="38" id="rect4961" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
<rect ry="3" rx="3" y="1010.3622" x="-37" height="10" width="38" id="rect4963" style="opacity:1;fill:#ffffff;fill-opacity:0.29803923;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
<rect ry="2.5384617" rx="3" y="1011.3622" x="-37" height="11" width="38" id="rect4965" style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
</g>
<g id="g4979">
<rect style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4146" width="38" height="26" x="5" y="1024.3622" rx="3" ry="3"/>
<rect ry="3" rx="3" y="1037.3622" x="5" height="13" width="38" id="rect4973" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
<rect ry="3" rx="3" y="1024.3622" x="5" height="13" width="38" id="rect4975" style="opacity:1;fill:#ffffff;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
<rect ry="2.7692308" rx="3" y="1025.3622" x="5" height="24" width="38" id="rect4977" style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
</g>
<g transform="translate(0,1013.3622)" id="g4211">
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0d47a1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 24,17.75 c -2.880662,0 -5.319789,1.984685 -6.033203,4.650391 l 3.212891,0 C 21.734004,21.415044 22.774798,20.75 24,20.75 c 1.812692,0 3.25,1.437308 3.25,3.25 0,1.812693 -1.437308,3.25 -3.25,3.25 -1.307381,0 -2.411251,-0.75269 -2.929688,-1.849609 l -3.154296,0 C 18.558263,28.166146 21.04791,30.25 24,30.25 c 3.434013,0 6.25,-2.815987 6.25,-6.25 0,-3.434012 -2.815987,-6.25 -6.25,-6.25 z" id="path4161" inkscape:connector-curvature="0"/>
<circle style="opacity:1;fill:none;fill-opacity:0.40392157;stroke:#0d47a1;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="path4209" cx="24" cy="24" r="9.5500002"/>
</g>
<g id="g4989" transform="translate(0,0.50001738)">
<ellipse cy="1016.4872" cx="14.375" id="circle4985" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" rx="3.375" ry="3.875"/>
<circle style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="path4859" cx="14.375" cy="1016.9872" r="3.375"/>
</g>
<g transform="translate(19.5,0.50001738)" id="g4171">
<ellipse ry="3.875" rx="3.375" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="ellipse4175" cx="14.375" cy="1016.4872"/>
<circle r="3.375" cy="1016.9872" cx="14.375" id="circle4177" style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117"/>
</g>
</g>
<path inkscape:connector-curvature="0" id="path5128" d="m 2.613462,1005.5987 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient5220);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
</g>
</svg>

After

(image error) Size: 21 KiB

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 511.999 511.999" xml:space="preserve">
<g>
<path style="fill:#32BBFF;" d="M382.369,175.623C322.891,142.356,227.427,88.937,79.355,6.028
C69.372-0.565,57.886-1.429,47.962,1.93l254.05,254.05L382.369,175.623z"/>
<path style="fill:#32BBFF;" d="M47.962,1.93c-1.86,0.63-3.67,1.39-5.401,2.308C31.602,10.166,23.549,21.573,23.549,36v439.96
c0,14.427,8.052,25.834,19.012,31.761c1.728,0.917,3.537,1.68,5.395,2.314L302.012,255.98L47.962,1.93z"/>
<path style="fill:#32BBFF;" d="M302.012,255.98L47.956,510.035c9.927,3.384,21.413,2.586,31.399-4.103
c143.598-80.41,237.986-133.196,298.152-166.746c1.675-0.941,3.316-1.861,4.938-2.772L302.012,255.98z"/>
</g>
<path style="fill:#2C9FD9;" d="M23.549,255.98v219.98c0,14.427,8.052,25.834,19.012,31.761c1.728,0.917,3.537,1.68,5.395,2.314
L302.012,255.98H23.549z"/>
<path style="fill:#29CC5E;" d="M79.355,6.028C67.5-1.8,53.52-1.577,42.561,4.239l255.595,255.596l84.212-84.212
C322.891,142.356,227.427,88.937,79.355,6.028z"/>
<path style="fill:#D93F21;" d="M298.158,252.126L42.561,507.721c10.96,5.815,24.939,6.151,36.794-1.789
c143.598-80.41,237.986-133.196,298.152-166.746c1.675-0.941,3.316-1.861,4.938-2.772L298.158,252.126z"/>
<path style="fill:#FFD500;" d="M488.45,255.98c0-12.19-6.151-24.492-18.342-31.314c0,0-22.799-12.721-92.682-51.809l-83.123,83.123
l83.204,83.205c69.116-38.807,92.6-51.892,92.6-51.892C482.299,280.472,488.45,268.17,488.45,255.98z"/>
<path style="fill:#FFAA00;" d="M470.108,287.294c12.191-6.822,18.342-19.124,18.342-31.314H294.303l83.204,83.205
C446.624,300.379,470.108,287.294,470.108,287.294z"/>
</svg>

After

(image error) Size: 1.8 KiB

@ -66,8 +66,30 @@
<a
class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/"
><i class="fa fa-mug-hot"></i> Code</a
><i class="fa fa-mug-hot"></i> Development</a
>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
on F-Droid</a
>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
>
<img src="googleplay.svg" style="height: 2em; margin: 0" />
Get it on Google Play (Open Testing)
</a>
</p>
</div>
<div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
@ -88,36 +110,31 @@
<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
>Download</a
>
Tilde Friends and run your own instance, or use
Tilde Friends or use
<a href="https://www.tildefriends.net/"
>https://www.tildefriends.net/</a
>.
</li>
<li>
Create an account to identify yourself with that instance by
username and password.
</li>
<li>
Create an SSB identity in the <b>ssb</b> app. This will generate a
keypair used to identify yourself to other users and sign your
messages so that they can be verified as from you.
</li>
<li>Create an account to identify yourself with that instance.</li>
<li>
Describe yourself in your profile in the <b>ssb</b> app. Give
yourself a name and an avatar if you like.
</li>
<li>
Connect to others. You will automatically discover peers on the
same instance and same network if there are any. Or use
<a href="https://github.com/staltz/ssb-room/blob/master/FAQ.md"
>rooms</a
>
and pubs to reach more distant users.
<a href="https://www.tildefriends.net/~cory/room/"
>tildefriends.net itself</a
>
operates as a room, so you can connect and see who else is online
and establish a connection.
Connect to others.
<ul>
<li>Automatically discover peers on the same network.</li>
<li>
Manually connect to rooms and pubs, including
<a href="https://www.tildefriends.net/~cory/room/"
>tildefriends.net itself</a
>.
</li>
<li>
Enable <b>Peer Exchange</b> in the <b>admin</b> to discover
internet peers.
</li>
</ul>
</li>
<li>Follow people to grow your network.</li>
<li>
@ -206,8 +223,13 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
<p>Tilde Friends is built using boring, trusted tech.</p>
<h1 class="w3-jumbo"><b>Boring Technology</b></h1>
<p>
Tilde Friends is built using boring, trusted tech. Unless a better
reason presents itself, it strives to use only simple and widely adopted
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits.
@ -244,7 +266,7 @@
<i class="fa fa-lock w3-text-purple w3-jumbo"></i>
<p>libsodium</p>
</a>
<a href="https://www.openssl.org/" class="w3-col s3">
<a href="https://github.com/openssl/openssl/releases" class="w3-col s3">
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
<p>OpenSSL</p>
</a>
@ -270,6 +292,13 @@
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
<p>Lit</p>
</a>
<a href="https://github.com/c-ares/c-ares" class="w3-col s3">
<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
<p>c-ares</p>
</a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
<p>GNU Make</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,7 +10,7 @@ let gSessionIndex = 0;
* @returns
*/
function makeSessionId() {
return (gSessionIndex++).toString();
return 'session_' + (gSessionIndex++).toString();
}
/**
@ -172,11 +172,7 @@ async function socket(request, response, client) {
0x1
);
} else {
process = await core.getSessionProcessBlob(
blobId,
sessionId,
options
);
process = await core.getProcessBlob(blobId, sessionId, options);
}
}
if (process) {

@ -382,8 +382,8 @@ class TfNavigationElement extends LitElement {
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
<span @click=${self.clear_error} class="w3-button w3-display-topright">&times;</span>
<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p style="white-space: pre">${this.status.message}</p></div>
<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">&times;</span>
<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
</div>
</div>
`
@ -1782,10 +1782,11 @@ async function sourcePretty() {
let prettier = (await import('/prettier/standalone.mjs')).default;
let babel = (await import('/prettier/babel.mjs')).default;
let estree = (await import('/prettier/estree.mjs')).default;
let prettier_html = (await import('/prettier/html.mjs')).default;
let source = gEditor.state.doc.toString();
let formatted = await prettier.format(source, {
parser: 'babel',
plugins: [babel, estree],
parser: gCurrentFile?.toLowerCase()?.endsWith('.html') ? 'html' : 'babel',
plugins: [babel, estree, prettier_html],
trailingComma: 'es5',
useTabs: true,
semi: true,
@ -1820,8 +1821,8 @@ function toggleVisibleWhitespace() {
.cm-highlightTab {
background-image: unset !important;
}
.cm-highlightSpace:before {
content: unset !important;
.cm-highlightSpace {
background-image: unset !important;
}
`;
window.localStorage.setItem('visible_whitespace', '1');

@ -77,7 +77,8 @@ const k_global_settings = {
peer_exchange: {
type: 'boolean',
default_value: false,
description: 'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
description:
'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
},
account_registration: {
type: 'boolean',
@ -86,10 +87,6 @@ const k_global_settings = {
},
};
let gGlobalSettings = {
index: '/~core/apps/',
};
let kPingInterval = 60 * 1000;
/**
@ -261,23 +258,6 @@ function postMessageInternal(from, to, message) {
}
}
/**
* TODOC
* @param {*} blobId
* @param {*} session
* @param {*} options
* @returns
*/
async function getSessionProcessBlob(blobId, session, options) {
let actualOptions = {timeout: kPingInterval};
if (options) {
for (let i in options) {
actualOptions[i] = options[i];
}
}
return getProcessBlob(blobId, 'session_' + session, actualOptions);
}
/**
* TODOC
* @param {*} blobId
@ -305,7 +285,7 @@ async function getProcessBlob(blobId, key, options) {
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = options.timeout;
process.timeout = kPingInterval;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@ -344,59 +324,59 @@ async function getProcessBlob(blobId, key, options) {
return [];
}
},
permissionsGranted: function () {
permissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
gGlobalSettings.userPermissions &&
gGlobalSettings.userPermissions[user] &&
gGlobalSettings.userPermissions[user][options.packageOwner]
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner]
) {
return gGlobalSettings.userPermissions[user][
options.packageOwner
][options.packageName];
return settings.userPermissions[user][options.packageOwner][
options.packageName
];
}
},
allPermissionsGranted: function () {
allPermissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
gGlobalSettings.userPermissions &&
gGlobalSettings.userPermissions[user]
settings.userPermissions &&
settings.userPermissions[user]
) {
return gGlobalSettings.userPermissions[user];
return settings.userPermissions[user];
}
},
permissionsForUser: function (user) {
return (
(gGlobalSettings?.permissions
? gGlobalSettings.permissions[user]
: []) ?? []
);
permissionsForUser: async function (user) {
let settings = await loadSettings();
return settings?.permissions?.[user] ?? [];
},
apps: (user) => getApps(user, process),
getSockets: getSockets,
permissionTest: function (permission) {
permissionTest: async function (permission) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (!user || !options?.packageOwner || !options?.packageName) {
return;
} else if (
gGlobalSettings.userPermissions &&
gGlobalSettings.userPermissions[user] &&
gGlobalSettings.userPermissions[user][options.packageOwner] &&
gGlobalSettings.userPermissions[user][options.packageOwner][
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
] &&
gGlobalSettings.userPermissions[user][options.packageOwner][
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission] !== undefined
) {
if (
gGlobalSettings.userPermissions[user][options.packageOwner][
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission]
) {
@ -407,9 +387,9 @@ async function getProcessBlob(blobId, key, options) {
} else if (process.app) {
return process.app
.makeFunction(['requestPermission'])(permission)
.then(function (value) {
.then(async function (value) {
if (value == 'allow') {
storePermission(
await ssb.setUserPermission(
user,
options.packageOwner,
options.packageName,
@ -421,7 +401,7 @@ async function getProcessBlob(blobId, key, options) {
} else if (value == 'allow once') {
return true;
} else if (value == 'deny') {
storePermission(
await ssb.setUserPermission(
user,
options.packageOwner,
options.packageName,
@ -508,23 +488,24 @@ async function getProcessBlob(blobId, key, options) {
}
};
if (process.credentials?.permissions?.administration) {
imports.core.globalSettingsDescriptions = function () {
imports.core.globalSettingsDescriptions = async function () {
let settings = Object.assign({}, k_global_settings);
for (let [key, value] of Object.entries(gGlobalSettings)) {
for (let [key, value] of Object.entries(await loadSettings())) {
if (settings[key]) {
settings[key].value = value;
}
}
return settings;
};
imports.core.globalSettingsGet = function (key) {
return gGlobalSettings[key];
imports.core.globalSettingsGet = async function (key) {
let settings = await loadSettings();
return settings?.[key];
};
imports.core.globalSettingsSet = async function (key, value) {
print('Setting', key, value);
await loadSettings();
gGlobalSettings[key] = value;
setGlobalSettings(gGlobalSettings);
let settings = await loadSettings();
settings[key] = value;
await new Database('core').set('settings', JSON.stringify(settings));
print('Done.');
};
imports.core.deleteUser = async function (user) {
@ -691,11 +672,24 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.swapWithServerIdentity(
process.credentials.session.name,
id
);
}
};
imports.ssb.addEventListener = undefined;
imports.ssb.removeEventListener = undefined;
imports.ssb.getIdentityInfo = undefined;
imports.fetch = function (url, options) {
return http.fetch(url, options, gGlobalSettings.fetch_hosts);
imports.fetch = async function (url, options) {
let settings = await loadSettings();
return http.fetch(url, options, settings?.fetch_hosts);
};
if (
@ -722,12 +716,12 @@ async function getProcessBlob(blobId, key, options) {
Object.keys(db).map((x) => [x, db[x].bind(db)])
);
};
imports.databases = function () {
imports.databases = async function () {
return [].concat(
databases.list(
await databases.list(
':shared:' + process.credentials.session.name + ':%'
),
databases.list(process.credentials.session.name + ':%')
await databases.list(process.credentials.session.name + ':%')
);
};
}
@ -746,22 +740,22 @@ async function getProcessBlob(blobId, key, options) {
);
};
}
process.sendPermissions = function sendPermissions() {
process.sendPermissions = async function sendPermissions() {
process.app.send({
action: 'permissions',
permissions: imports.core.permissionsGranted(),
permissions: await imports.core.permissionsGranted(),
});
};
process.resetPermission = function resetPermission(permission) {
process.resetPermission = async function resetPermission(permission) {
let user = process?.credentials?.session?.name;
storePermission(
await ssb.setUserPermission(
user,
options?.packageOwner,
options?.packageName,
permission,
undefined
);
process.sendPermissions();
return process.sendPermissions();
};
process.task.setImports(imports);
process.task.activate();
@ -794,7 +788,7 @@ async function getProcessBlob(blobId, key, options) {
broadcastEvent('onSessionBegin', [getUser(process, process)]);
if (process.app) {
process.app.send({action: 'ready', version: version()});
process.sendPermissions();
await process.sendPermissions();
}
await process.task.execute({name: appSourceName, source: appSource});
resolveReady(process);
@ -818,20 +812,6 @@ async function getProcessBlob(blobId, key, options) {
return process;
}
/**
* TODOC
* @param {*} settings
* @returns
*/
async function setGlobalSettings(settings) {
gGlobalSettings = settings;
try {
return await new Database('core').set('settings', JSON.stringify(settings));
} catch (error) {
print('Error storing settings:', error);
}
}
/**
* TODOC
* @param {*} response
@ -952,237 +932,83 @@ async function blobHandler(request, response, blobId, uri) {
}
let process;
if (uri == '/view') {
let data;
let match;
let query = form.decodeForm(request.query);
let headers = {
'Content-Security-Policy': k_content_security_policy,
};
if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
}
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let id = await new Database(match[1]).get('path:' + match[2]);
if (id) {
if (request.headers['if-none-match'] === '"' + id + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
data = await ssb.blobGet(id);
if (match[3]) {
let appObject = JSON.parse(data);
data = appObject.files[match[3]];
}
sendData(
response,
data,
undefined,
Object.assign({etag: '"' + id + '"'}, headers)
);
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
sendData(
response,
data,
undefined,
Object.assign({etag: '"' + blobId + '"'}, headers)
);
}
}
} else {
if (request.headers['if-none-match'] === '"' + blobId + '"') {
headers['Content-Length'] = '0';
response.writeHead(304, headers);
response.end();
} else {
data = await ssb.blobGet(blobId);
sendData(
response,
data,
undefined,
Object.assign({etag: '"' + blobId + '"'}, headers)
);
}
}
} else if (uri == '/save') {
let match;
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1];
let appName = match[2];
let credentials = await httpd.auth_query(request.headers);
if (
credentials &&
credentials.session &&
(credentials.session.name == user ||
(credentials.permissions.administration && user == 'core'))
) {
let database = new Database(user);
let data;
let match;
let id;
let app_id = blobId;
let packageOwner;
let packageName;
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
packageOwner = match[1];
packageName = match[2];
let db = new Database(match[1]);
app_id = await db.get('path:' + match[2]);
}
let app_object = JSON.parse(utf8Decode(request.body));
let previous_id = await database.get('path:' + appName);
if (previous_id) {
try {
let previous_object = JSON.parse(
utf8Decode(await ssb.blobGet(previous_id))
);
delete previous_object.previous;
delete app_object.previous;
if (JSON.stringify(previous_object) == JSON.stringify(app_object)) {
response.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
});
response.end('/' + previous_id);
return;
}
} catch {}
}
app_object.previous = previous_id;
let newBlobId = await ssb.blobStore(JSON.stringify(app_object));
let apps = new Set();
let apps_original = await database.get('apps');
try {
apps = new Set(JSON.parse(apps_original));
} catch {}
if (!apps.has(appName)) {
apps.add(appName);
}
apps = JSON.stringify([...apps].sort());
if (apps != apps_original) {
await database.set('apps', apps);
}
await database.set('path:' + appName, newBlobId);
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('/' + newBlobId);
} else {
response.writeHead(401, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('401 Unauthorized');
return;
}
} else if (blobId === '') {
let newBlobId = await ssb.blobStore(request.body);
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('/' + newBlobId);
} else {
response.writeHead(400, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('Invalid name.');
}
} else if (uri == '/delete') {
let match;
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
let user = match[1];
let appName = match[2];
let credentials = await httpd.auth_query(request.headers);
if (
credentials &&
credentials.session &&
(credentials.session.name == user ||
(credentials.permissions.administration && user == 'core'))
) {
let database = new Database(user);
let apps = new Set();
try {
apps = new Set(JSON.parse(await database.get('apps')));
} catch {}
if (apps.delete(appName)) {
await database.set('apps', JSON.stringify([...apps].sort()));
}
database.remove('path:' + appName);
} else {
response.writeHead(401, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('401 Unauthorized');
return;
}
}
response.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
response.end('OK');
} else {
let data;
let match;
let id;
let app_id = blobId;
let packageOwner;
let packageName;
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
packageOwner = match[1];
packageName = match[2];
let db = new Database(match[1]);
app_id = await db.get('path:' + match[2]);
}
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
id = app_object?.files[uri.substring(1)];
if (!id && app_object?.files['handler.js']) {
let answer;
try {
answer = await useAppHandler(
response,
app_id,
uri.substring(1),
request.query ? form.decodeForm(request.query) : undefined,
request.headers,
packageOwner,
packageName
);
} catch (error) {
data = utf8Encode(
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
);
response.writeHead(500, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length,
});
response.end(data);
return;
}
if (answer && typeof answer.data == 'string') {
answer.data = utf8Encode(answer.data);
}
sendData(
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
id = app_object?.files[uri.substring(1)];
if (!id && app_object?.files['handler.js']) {
let answer;
try {
answer = await useAppHandler(
response,
answer?.data,
answer?.content_type,
Object.assign(answer?.headers ?? {}, {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
}),
answer.status_code
app_id,
uri.substring(1),
request.query ? form.decodeForm(request.query) : undefined,
request.headers,
packageOwner,
packageName
);
} else if (id) {
if (
request.headers['if-none-match'] &&
request.headers['if-none-match'] == '"' + id + '"'
) {
let headers = {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
'Content-Length': '0',
};
response.writeHead(304, headers);
response.end();
} else {
let headers = {
ETag: '"' + id + '"',
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
};
data = await ssb.blobGet(id);
let type =
httpd.mime_type_from_extension(uri) ||
httpd.mime_type_from_magic_bytes(data);
sendData(response, data, type, headers);
}
} else {
sendData(response, data, undefined, {});
} catch (error) {
data = utf8Encode(
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
);
response.writeHead(500, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length,
});
response.end(data);
return;
}
if (answer && typeof answer.data == 'string') {
answer.data = utf8Encode(answer.data);
}
sendData(
response,
answer?.data,
answer?.content_type,
Object.assign(answer?.headers ?? {}, {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
}),
answer.status_code
);
} else if (id) {
if (
request.headers['if-none-match'] &&
request.headers['if-none-match'] == '"' + id + '"'
) {
let headers = {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
'Content-Length': '0',
};
response.writeHead(304, headers);
response.end();
} else {
let headers = {
ETag: '"' + id + '"',
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
};
data = await ssb.blobGet(id);
let type =
httpd.mime_type_from_extension(uri) ||
httpd.mime_type_from_magic_bytes(data);
sendData(response, data, type, headers);
}
} else {
sendData(response, data, undefined, {});
}
}
@ -1216,7 +1042,7 @@ async function loadSettings() {
data[key] = value.default_value;
}
}
gGlobalSettings = data;
return data;
}
/**
@ -1241,9 +1067,9 @@ function sendStats() {
* TODOC
*/
loadSettings()
.then(function () {
if (tildefriends.https_port && gGlobalSettings.http_redirect) {
httpd.set_http_redirect(gGlobalSettings.http_redirect);
.then(function (settings) {
if (tildefriends.https_port && settings.http_redirect) {
httpd.set_http_redirect(settings.http_redirect);
}
httpd.all('/app/socket', app.socket);
httpd.all('', function default_http_handler(request, response) {
@ -1254,15 +1080,6 @@ loadSettings()
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
) {
return blobHandler(request, response, match[1], match[2]);
} else if ((match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri))) {
return blobHandler(request, response, match[1], match[2]);
} else {
let data = 'File not found.';
response.writeHead(404, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length.toString(),
});
return response.end(data);
}
});
let port = httpd.start(tildefriends.http_port);
@ -1311,47 +1128,4 @@ loadSettings()
exit(1);
});
/**
* TODOC
* @param {*} user
* @param {*} packageOwner
* @param {*} packageName
* @param {*} permission
* @param {*} allow
*/
function storePermission(user, packageOwner, packageName, permission, allow) {
if (!gGlobalSettings.userPermissions) {
gGlobalSettings.userPermissions = {};
}
if (!gGlobalSettings.userPermissions[user]) {
gGlobalSettings.userPermissions[user] = {};
}
if (!gGlobalSettings.userPermissions[user][packageOwner]) {
gGlobalSettings.userPermissions[user][packageOwner] = {};
}
if (!gGlobalSettings.userPermissions[user][packageOwner][packageName]) {
gGlobalSettings.userPermissions[user][packageOwner][packageName] = {};
}
if (
gGlobalSettings.userPermissions[user][packageOwner][packageName][
permission
] !== allow
) {
if (allow === undefined) {
delete gGlobalSettings.userPermissions[user][packageOwner][packageName][
permission
];
} else {
gGlobalSettings.userPermissions[user][packageOwner][packageName][
permission
] = allow;
}
setGlobalSettings(gGlobalSettings);
}
}
export {
gGlobalSettings as globalSettings,
invoke,
getSessionProcessBlob,
};
export {invoke, getProcessBlob};

@ -21,14 +21,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.22";
version = "0.0.23";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-0x4LJXtMUxBsWEOoWAaQlStTfvqUf4Qs/5vJZ2EvBpY=";
hash = "sha256-ukZpi+BXRTFGbdvd5ApmctTo8bjtPJMHjqFPgVSyBWU=";
fetchSubmodules = true;
};

2
deps/c-ares vendored

File diff suppressed because one or more lines are too long

260
deps/codemirror_src/package-lock.json generated vendored

@ -19,9 +19,9 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.0.tgz",
"integrity": "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==",
"version": "6.18.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
"integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@ -37,9 +37,9 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
"integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz",
"integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@ -49,16 +49,16 @@
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz",
"integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.0.0"
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-html": {
@ -104,9 +104,9 @@
}
},
"node_modules/@codemirror/language": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz",
"integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@ -118,9 +118,9 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
"integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz",
"integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@ -158,9 +158,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.33.0",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.33.0.tgz",
"integrity": "sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==",
"version": "6.34.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz",
"integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.4.0",
@ -233,15 +233,15 @@
}
},
"node_modules/@lezer/common": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.8.tgz",
"integrity": "sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==",
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@ -270,9 +270,9 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.17",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz",
"integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==",
"version": "1.4.19",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@ -301,15 +301,14 @@
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
"integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==",
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-builtin-module": "^3.2.1",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
@ -349,14 +348,14 @@
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
"integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz",
"integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==",
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^2.3.1"
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
@ -371,9 +370,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
"integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
"integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
"cpu": [
"arm"
],
@ -384,9 +383,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
"integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
"integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
"cpu": [
"arm64"
],
@ -397,9 +396,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
"integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
"integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
"cpu": [
"arm64"
],
@ -410,9 +409,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
"integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
"integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
"cpu": [
"x64"
],
@ -423,9 +422,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
"integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
"integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
"cpu": [
"arm"
],
@ -436,9 +435,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
"integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
"integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
"cpu": [
"arm"
],
@ -449,9 +448,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
"integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
"integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
"cpu": [
"arm64"
],
@ -462,9 +461,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
"integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
"integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
"cpu": [
"arm64"
],
@ -475,9 +474,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
"integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
"integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
"cpu": [
"ppc64"
],
@ -488,9 +487,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
"integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
"integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
"cpu": [
"riscv64"
],
@ -501,9 +500,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
"integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
"integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
"cpu": [
"s390x"
],
@ -514,9 +513,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
"integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
"integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
"cpu": [
"x64"
],
@ -527,9 +526,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
"integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
"integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
"cpu": [
"x64"
],
@ -540,9 +539,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
"integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
"integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
"cpu": [
"arm64"
],
@ -553,9 +552,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
"integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
"integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
"cpu": [
"ia32"
],
@ -566,9 +565,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
"integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
"integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
"cpu": [
"x64"
],
@ -579,9 +578,9 @@
]
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/resolve": {
@ -591,9 +590,9 @@
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
"dev": true,
"license": "MIT",
"bin": {
@ -610,18 +609,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
@ -700,21 +687,6 @@
"node": ">= 0.4"
}
},
"node_modules/is-builtin-module": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
"license": "MIT",
"dependencies": {
"builtin-modules": "^3.3.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
@ -743,12 +715,12 @@
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=8.6"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@ -782,12 +754,12 @@
}
},
"node_modules/rollup": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
"integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
"integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
@ -797,22 +769,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.21.0",
"@rollup/rollup-android-arm64": "4.21.0",
"@rollup/rollup-darwin-arm64": "4.21.0",
"@rollup/rollup-darwin-x64": "4.21.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
"@rollup/rollup-linux-arm-musleabihf": "4.21.0",
"@rollup/rollup-linux-arm64-gnu": "4.21.0",
"@rollup/rollup-linux-arm64-musl": "4.21.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
"@rollup/rollup-linux-riscv64-gnu": "4.21.0",
"@rollup/rollup-linux-s390x-gnu": "4.21.0",
"@rollup/rollup-linux-x64-gnu": "4.21.0",
"@rollup/rollup-linux-x64-musl": "4.21.0",
"@rollup/rollup-win32-arm64-msvc": "4.21.0",
"@rollup/rollup-win32-ia32-msvc": "4.21.0",
"@rollup/rollup-win32-x64-msvc": "4.21.0",
"@rollup/rollup-android-arm-eabi": "4.24.0",
"@rollup/rollup-android-arm64": "4.24.0",
"@rollup/rollup-darwin-arm64": "4.24.0",
"@rollup/rollup-darwin-x64": "4.24.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
"@rollup/rollup-linux-arm-musleabihf": "4.24.0",
"@rollup/rollup-linux-arm64-gnu": "4.24.0",
"@rollup/rollup-linux-arm64-musl": "4.24.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
"@rollup/rollup-linux-riscv64-gnu": "4.24.0",
"@rollup/rollup-linux-s390x-gnu": "4.24.0",
"@rollup/rollup-linux-x64-gnu": "4.24.0",
"@rollup/rollup-linux-x64-musl": "4.24.0",
"@rollup/rollup-win32-arm64-msvc": "4.24.0",
"@rollup/rollup-win32-ia32-msvc": "4.24.0",
"@rollup/rollup-win32-x64-msvc": "4.24.0",
"fsevents": "~2.3.2"
}
},
@ -894,9 +866,9 @@
}
},
"node_modules/terser": {
"version": "5.31.6",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
"integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
"version": "5.36.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {

2
deps/libuv vendored

Submodule deps/libuv updated: e9f29cb984...e1095c7a43

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6000
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

8303
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

193
deps/sqlite/sqlite3.h vendored

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.46.1"
#define SQLITE_VERSION_NUMBER 3046001
#define SQLITE_SOURCE_ID "2024-08-13 09:16:08 c9c2ab54ba1f5f46360f1b4f35d849cd3f080e6fc2b6c60e91b16c63f69a1e33"
#define SQLITE_VERSION "3.47.0"
#define SQLITE_VERSION_NUMBER 3047000
#define SQLITE_SOURCE_ID "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -772,8 +772,8 @@ struct sqlite3_file {
** to xUnlock() is a no-op.
** The xCheckReservedLock() method checks whether any database connection,
** either in this process or in some other process, is holding a RESERVED,
** PENDING, or EXCLUSIVE lock on the file. It returns true
** if such a lock exists and false otherwise.
** PENDING, or EXCLUSIVE lock on the file. It returns, via its output
** pointer parameter, true if such a lock exists and false otherwise.
**
** The xFileControl() method is a generic interface that allows custom
** VFS implementations to directly control an open file using the
@ -3570,8 +3570,8 @@ SQLITE_API void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*);
**
** [[OPEN_EXRESCODE]] ^(<dt>[SQLITE_OPEN_EXRESCODE]</dt>
** <dd>The database connection comes up in "extended result code mode".
** In other words, the database behaves has if
** [sqlite3_extended_result_codes(db,1)] where called on the database
** In other words, the database behaves as if
** [sqlite3_extended_result_codes(db,1)] were called on the database
** connection as soon as the connection is created. In addition to setting
** the extended result code mode, this flag also causes [sqlite3_open_v2()]
** to return an extended result code.</dd>
@ -4222,13 +4222,17 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
** and sqlite3_prepare16_v3() use UTF-16.
**
** ^If the nByte argument is negative, then zSql is read up to the
** first zero terminator. ^If nByte is positive, then it is the
** number of bytes read from zSql. ^If nByte is zero, then no prepared
** first zero terminator. ^If nByte is positive, then it is the maximum
** number of bytes read from zSql. When nByte is positive, zSql is read
** up to the first zero terminator or until the nByte bytes have been read,
** whichever comes first. ^If nByte is zero, then no prepared
** statement is generated.
** If the caller knows that the supplied string is nul-terminated, then
** there is a small performance advantage to passing an nByte parameter that
** is the number of bytes in the input string <i>including</i>
** the nul-terminator.
** Note that nByte measure the length of the input in bytes, not
** characters, even for the UTF-16 interfaces.
**
** ^If pzTail is not NULL then *pzTail is made to point to the first byte
** past the end of the first SQL statement in zSql. These routines only
@ -5599,7 +5603,7 @@ SQLITE_API int sqlite3_create_window_function(
** This flag instructs SQLite to omit some corner-case optimizations that
** might disrupt the operation of the [sqlite3_value_subtype()] function,
** causing it to return zero rather than the correct subtype().
** SQL functions that invokes [sqlite3_value_subtype()] should have this
** All SQL functions that invoke [sqlite3_value_subtype()] should have this
** property. If the SQLITE_SUBTYPE property is omitted, then the return
** value from [sqlite3_value_subtype()] might sometimes be zero even though
** a non-zero subtype was specified by the function argument expression.
@ -5615,6 +5619,15 @@ SQLITE_API int sqlite3_create_window_function(
** [sqlite3_result_subtype()] should avoid setting this property, as the
** purpose of this property is to disable certain optimizations that are
** incompatible with subtypes.
**
** [[SQLITE_SELFORDER1]] <dt>SQLITE_SELFORDER1</dt><dd>
** The SQLITE_SELFORDER1 flag indicates that the function is an aggregate
** that internally orders the values provided to the first argument. The
** ordered-set aggregate SQL notation with a single ORDER BY term can be
** used to invoke this function. If the ordered-set aggregate notation is
** used on a function that lacks this flag, then an error is raised. Note
** that the ordered-set aggregate syntax is only available if SQLite is
** built using the -DSQLITE_ENABLE_ORDERED_SET_AGGREGATES compile-time option.
** </dd>
** </dl>
*/
@ -5623,6 +5636,7 @@ SQLITE_API int sqlite3_create_window_function(
#define SQLITE_SUBTYPE 0x000100000
#define SQLITE_INNOCUOUS 0x000200000
#define SQLITE_RESULT_SUBTYPE 0x001000000
#define SQLITE_SELFORDER1 0x002000000
/*
** CAPI3REF: Deprecated Functions
@ -5820,7 +5834,7 @@ SQLITE_API int sqlite3_value_encoding(sqlite3_value*);
** one SQL function to another. Use the [sqlite3_result_subtype()]
** routine to set the subtype for the return value of an SQL function.
**
** Every [application-defined SQL function] that invoke this interface
** Every [application-defined SQL function] that invokes this interface
** should include the [SQLITE_SUBTYPE] property in the text
** encoding argument when the function is [sqlite3_create_function|registered].
** If the [SQLITE_SUBTYPE] property is omitted, then sqlite3_value_subtype()
@ -7427,9 +7441,11 @@ struct sqlite3_module {
** will be returned by the strategy.
**
** The xBestIndex method may optionally populate the idxFlags field with a
** mask of SQLITE_INDEX_SCAN_* flags. Currently there is only one such flag -
** SQLITE_INDEX_SCAN_UNIQUE. If the xBestIndex method sets this flag, SQLite
** assumes that the strategy may visit at most one row.
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
** output to show the idxNum has hex instead of as decimal. Another flag is
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
** return at most one row.
**
** Additionally, if xBestIndex sets the SQLITE_INDEX_SCAN_UNIQUE flag, then
** SQLite also assumes that if a call to the xUpdate() method is made as
@ -7493,7 +7509,9 @@ struct sqlite3_index_info {
** [sqlite3_index_info].idxFlags field to some combination of
** these bits.
*/
#define SQLITE_INDEX_SCAN_UNIQUE 1 /* Scan visits at most 1 row */
#define SQLITE_INDEX_SCAN_UNIQUE 0x00000001 /* Scan visits at most 1 row */
#define SQLITE_INDEX_SCAN_HEX 0x00000002 /* Display idxNum as hex */
/* in EXPLAIN QUERY PLAN */
/*
** CAPI3REF: Virtual Table Constraint Operator Codes
@ -8330,6 +8348,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
#define SQLITE_TESTCTRL_JSON_SELFCHECK 14
#define SQLITE_TESTCTRL_OPTIMIZATIONS 15
#define SQLITE_TESTCTRL_ISKEYWORD 16 /* NOT USED */
#define SQLITE_TESTCTRL_GETOPT 16
#define SQLITE_TESTCTRL_SCRATCHMALLOC 17 /* NOT USED */
#define SQLITE_TESTCTRL_INTERNAL_FUNCTIONS 17
#define SQLITE_TESTCTRL_LOCALTIME_FAULT 18
@ -8349,7 +8368,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
#define SQLITE_TESTCTRL_TRACEFLAGS 31
#define SQLITE_TESTCTRL_TUNE 32
#define SQLITE_TESTCTRL_LOGEST 33
#define SQLITE_TESTCTRL_USELONGDOUBLE 34
#define SQLITE_TESTCTRL_USELONGDOUBLE 34 /* NOT USED */
#define SQLITE_TESTCTRL_LAST 34 /* Largest TESTCTRL */
/*
@ -9325,6 +9344,16 @@ typedef struct sqlite3_backup sqlite3_backup;
** APIs are not strictly speaking threadsafe. If they are invoked at the
** same time as another thread is invoking sqlite3_backup_step() it is
** possible that they return invalid values.
**
** <b>Alternatives To Using The Backup API</b>
**
** Other techniques for safely creating a consistent backup of an SQLite
** database include:
**
** <ul>
** <li> The [VACUUM INTO] command.
** <li> The [sqlite3_rsync] utility program.
** </ul>
*/
SQLITE_API sqlite3_backup *sqlite3_backup_init(
sqlite3 *pDest, /* Destination database handle */
@ -10524,6 +10553,14 @@ typedef struct sqlite3_snapshot {
** If there is not already a read-transaction open on schema S when
** this function is called, one is opened automatically.
**
** If a read-transaction is opened by this function, then it is guaranteed
** that the returned snapshot object may not be invalidated by a database
** writer or checkpointer until after the read-transaction is closed. This
** is not guaranteed if a read-transaction is already open when this
** function is called. In that case, any subsequent write or checkpoint
** operation on the database may invalidate the returned snapshot handle,
** even while the read-transaction remains open.
**
** The following must be true for this function to succeed. If any of
** the following statements are false when sqlite3_snapshot_get() is
** called, SQLITE_ERROR is returned. The final value of *P is undefined
@ -10832,8 +10869,6 @@ SQLITE_API int sqlite3_deserialize(
#if defined(__wasi__)
# undef SQLITE_WASI
# define SQLITE_WASI 1
# undef SQLITE_OMIT_WAL
# define SQLITE_OMIT_WAL 1/* because it requires shared memory APIs */
# ifndef SQLITE_OMIT_LOAD_EXTENSION
# define SQLITE_OMIT_LOAD_EXTENSION
# endif
@ -13036,6 +13071,10 @@ struct Fts5PhraseIter {
** (i.e. if it is a contentless table), then this API always iterates
** through an empty set (all calls to xPhraseFirst() set iCol to -1).
**
** In all cases, matches are visited in (column ASC, offset ASC) order.
** i.e. all those in column 0, sorted by offset, followed by those in
** column 1, etc.
**
** xPhraseNext()
** See xPhraseFirst above.
**
@ -13102,9 +13141,32 @@ struct Fts5PhraseIter {
**
** This API can be quite slow if used with an FTS5 table created with the
** "detail=none" or "detail=column" option.
**
** xColumnLocale(pFts5, iIdx, pzLocale, pnLocale)
** If parameter iCol is less than zero, or greater than or equal to the
** number of columns in the table, SQLITE_RANGE is returned.
**
** Otherwise, this function attempts to retrieve the locale associated
** with column iCol of the current row. Usually, there is no associated
** locale, and output parameters (*pzLocale) and (*pnLocale) are set
** to NULL and 0, respectively. However, if the fts5_locale() function
** was used to associate a locale with the value when it was inserted
** into the fts5 table, then (*pzLocale) is set to point to a nul-terminated
** buffer containing the name of the locale in utf-8 encoding. (*pnLocale)
** is set to the size in bytes of the buffer, not including the
** nul-terminator.
**
** If successful, SQLITE_OK is returned. Or, if an error occurs, an
** SQLite error code is returned. The final value of the output parameters
** is undefined in this case.
**
** xTokenize_v2:
** Tokenize text using the tokenizer belonging to the FTS5 table. This
** API is the same as the xTokenize() API, except that it allows a tokenizer
** locale to be specified.
*/
struct Fts5ExtensionApi {
int iVersion; /* Currently always set to 3 */
int iVersion; /* Currently always set to 4 */
void *(*xUserData)(Fts5Context*);
@ -13146,6 +13208,15 @@ struct Fts5ExtensionApi {
const char **ppToken, int *pnToken
);
int (*xInstToken)(Fts5Context*, int iIdx, int iToken, const char**, int*);
/* Below this point are iVersion>=4 only */
int (*xColumnLocale)(Fts5Context*, int iCol, const char **pz, int *pn);
int (*xTokenize_v2)(Fts5Context*,
const char *pText, int nText, /* Text to tokenize */
const char *pLocale, int nLocale, /* Locale to pass to tokenizer */
void *pCtx, /* Context passed to xToken() */
int (*xToken)(void*, int, const char*, int, int, int) /* Callback */
);
};
/*
@ -13166,7 +13237,7 @@ struct Fts5ExtensionApi {
** A tokenizer instance is required to actually tokenize text.
**
** The first argument passed to this function is a copy of the (void*)
** pointer provided by the application when the fts5_tokenizer object
** pointer provided by the application when the fts5_tokenizer_v2 object
** was registered with FTS5 (the third argument to xCreateTokenizer()).
** The second and third arguments are an array of nul-terminated strings
** containing the tokenizer arguments, if any, specified following the
@ -13190,7 +13261,7 @@ struct Fts5ExtensionApi {
** argument passed to this function is a pointer to an Fts5Tokenizer object
** returned by an earlier call to xCreate().
**
** The second argument indicates the reason that FTS5 is requesting
** The third argument indicates the reason that FTS5 is requesting
** tokenization of the supplied text. This is always one of the following
** four values:
**
@ -13214,6 +13285,13 @@ struct Fts5ExtensionApi {
** on a columnsize=0 database.
** </ul>
**
** The sixth and seventh arguments passed to xTokenize() - pLocale and
** nLocale - are a pointer to a buffer containing the locale to use for
** tokenization (e.g. "en_US") and its size in bytes, respectively. The
** pLocale buffer is not nul-terminated. pLocale may be passed NULL (in
** which case nLocale is always 0) to indicate that the tokenizer should
** use its default locale.
**
** For each token in the input string, the supplied callback xToken() must
** be invoked. The first argument to it should be a copy of the pointer
** passed as the second argument to xTokenize(). The third and fourth
@ -13237,6 +13315,30 @@ struct Fts5ExtensionApi {
** may abandon the tokenization and return any error code other than
** SQLITE_OK or SQLITE_DONE.
**
** If the tokenizer is registered using an fts5_tokenizer_v2 object,
** then the xTokenize() method has two additional arguments - pLocale
** and nLocale. These specify the locale that the tokenizer should use
** for the current request. If pLocale and nLocale are both 0, then the
** tokenizer should use its default locale. Otherwise, pLocale points to
** an nLocale byte buffer containing the name of the locale to use as utf-8
** text. pLocale is not nul-terminated.
**
** FTS5_TOKENIZER
**
** There is also an fts5_tokenizer object. This is an older, deprecated,
** version of fts5_tokenizer_v2. It is similar except that:
**
** <ul>
** <li> There is no "iVersion" field, and
** <li> The xTokenize() method does not take a locale argument.
** </ul>
**
** Legacy fts5_tokenizer tokenizers must be registered using the
** legacy xCreateTokenizer() function, instead of xCreateTokenizer_v2().
**
** Tokenizer implementations registered using either API may be retrieved
** using both xFindTokenizer() and xFindTokenizer_v2().
**
** SYNONYM SUPPORT
**
** Custom tokenizers may also support synonyms. Consider a case in which a
@ -13345,6 +13447,33 @@ struct Fts5ExtensionApi {
** inefficient.
*/
typedef struct Fts5Tokenizer Fts5Tokenizer;
typedef struct fts5_tokenizer_v2 fts5_tokenizer_v2;
struct fts5_tokenizer_v2 {
int iVersion; /* Currently always 2 */
int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
void (*xDelete)(Fts5Tokenizer*);
int (*xTokenize)(Fts5Tokenizer*,
void *pCtx,
int flags, /* Mask of FTS5_TOKENIZE_* flags */
const char *pText, int nText,
const char *pLocale, int nLocale,
int (*xToken)(
void *pCtx, /* Copy of 2nd argument to xTokenize() */
int tflags, /* Mask of FTS5_TOKEN_* flags */
const char *pToken, /* Pointer to buffer containing token */
int nToken, /* Size of token in bytes */
int iStart, /* Byte offset of token within input text */
int iEnd /* Byte offset of end of token within input text */
)
);
};
/*
** New code should use the fts5_tokenizer_v2 type to define tokenizer
** implementations. The following type is included for legacy applications
** that still use it.
*/
typedef struct fts5_tokenizer fts5_tokenizer;
struct fts5_tokenizer {
int (*xCreate)(void*, const char **azArg, int nArg, Fts5Tokenizer **ppOut);
@ -13364,6 +13493,7 @@ struct fts5_tokenizer {
);
};
/* Flags that may be passed as the third argument to xTokenize() */
#define FTS5_TOKENIZE_QUERY 0x0001
#define FTS5_TOKENIZE_PREFIX 0x0002
@ -13383,7 +13513,7 @@ struct fts5_tokenizer {
*/
typedef struct fts5_api fts5_api;
struct fts5_api {
int iVersion; /* Currently always set to 2 */
int iVersion; /* Currently always set to 3 */
/* Create a new tokenizer */
int (*xCreateTokenizer)(
@ -13410,6 +13540,25 @@ struct fts5_api {
fts5_extension_function xFunction,
void (*xDestroy)(void*)
);
/* APIs below this point are only available if iVersion>=3 */
/* Create a new tokenizer */
int (*xCreateTokenizer_v2)(
fts5_api *pApi,
const char *zName,
void *pUserData,
fts5_tokenizer_v2 *pTokenizer,
void (*xDestroy)(void*)
);
/* Find an existing tokenizer */
int (*xFindTokenizer_v2)(
fts5_api *pApi,
const char *zName,
void **ppUserData,
fts5_tokenizer_v2 **ppTokenizer
);
};
/*

63
docs/cheatsheet.md Normal file

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

@ -1,209 +1,166 @@
# Philosophy
# Tilde Friends Developer's Guide
Tilde Friends is a platform for making, running, and sharing web applications.
A Tilde Friends application starts with code that runs on a Tilde Friends server, possibly far away from where you wrote it, in a little JavaScript environment, in its own restricted process, with the only access to the outside world being the ability to send messages to the server. This document gives some recipes showing how that can be used to build a functional user-facing application in light of the unique constraints present.
When you visit Tilde Friends in a web browser, you are presented with a
terminal interface, typically with a big text output box covering most of the
page and an input box at the bottom, into which text or commands can be
entered. A script runs to produce text output and consume user input.
## Example 1: Hello, world!
The script is a Tilde Friends application, and it runs on the server, which
means that unlike client-side JavaScript, it can have the ability to read and
write files on the server or create network connections to other machines.
Unlike node.js or other server-side runtime environments, applications are
limited for security reasons to not interfere with each other or bring the
entire server down.
Of course we must start with a classic.
Above the terminal, an "Edit" link brings a visitor to the source code for the
current Tilde Friends application, which they can then edit, save as their own,
and run.
### app.js
# Architecture
```
app.setDocument('<h1 style="color: #fff">Hello, world!</h1>');
```
Tilde Friends is a C++ application with a JavaScript runtime that provides
restricted access to filesystem, network, and other system resources. The core
process runs a core set of scripts that implement a web server, typically
starting a new process for each visitor's session which runs scripts for the
active application and stopping it when the visitor leaves.
### Output
Only the core process has access to most system resources, but session
processes can be given accesss through the core process.
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Hello, world!&lt;/h1&gt;"></iframe>
Service processes are identical to session processes, but they are not tied to
a user session.
### Explanation
## Communication
At a glance, this might seem mundane, but for it to work:
In the same way that web browsers expose APIs for scripts running in the
browser to modify the document, play sounds and video, and draw, Tilde Friends
exposes APIs for scripts running on a Tilde Friends server to interact with a
visitor's web browser, read and write files on the server, and otherwise
interact with the world.
- the server starts a real process for your app and loads your code into it
- your code runs
- `app.setDocument()` sends a message back to the server
- the server interprets the message and redirects it to the browser
- `core/client.js` in the browser receives the message and puts your HTML into an iframe
- your HTML is presented by the browser in an iframe sandbox
There are several distinct classes of APIs.
But you don't have to think about all that. Call a function, and you see the result.
First, there are low-level functions exposed from C++ to JavaScript. Most of
these are only available to the core process. These typically only go through
a basic JavaScript to C++ transition and are relatively fast and immediate.
## Example 2: Hit Counter
// Displays some text to the server's console.
print("Hello, world!");
Let's take advantage of code running on the server and create a little hit counter using a key value store shared between all visitors.
There is a mechanism for communicating between processes. Functions can be
exported and called across process boundaries. When this is done, any
arguments are serialized to a network protocol, deserialized by the other
process, the function called, and finally any return value is passed back in
the same way. Any functions referenced by the arguments or return value are
also exported and can be subsequently called across process boundaries.
Functions called across process boundaries are always asynchronous, returning a
Promise. Care must be taken for security reasons to not pass dangerous
functions ("deleteAllMydata()") to untrusted processes, and it is best for
performance reasons to minimize the data size transferred between processes.
### app.js
// Send an "add" function to any other running processes. When called, it
// will run in this process.
core.broadcast({add: function(x, y) { return x + y; }});
```
async function main() {
let db = await shared_database('visitors');
let count = parseInt((await db.get('visitors')) ?? '0') + 1;
await db.set('visitors', count.toString());
await app.setDocument(`
<h1 style="color: #fff">Welcome, visitor #${count}!</h1>
`);
}
// Receive the above message and call the function.
core.register("onMessage", function(sender, message) {
message.add(3, 4).then(x => terminal.print(x.toString()));
});
main();
```
Finally, there is a core web interface that runs on the client's browser that
extends access to a running Tilde Friends script.
### Output
// Displays a message in the client's browser.
terminal.print("Hello, world!");
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Welcome, visitor #1!&lt;/h1&gt;"></iframe>
## API Documentation
### Explanation
The Tilde Friends API is very much evolving.
Just as pure browser apps have access to `localStorage`, Tilde Friends apps have access to key-value storage on the server.
All currently registered methods can be explored in the
[documentation](https://www.tildefriends.net/~cory/documentation) app.
The interface is a bit clunky and will likely change someday, but this example gets a database object, from which you can get and set string values by key. There are various on `shared_database` that let you store data that is private to the user or shared by different criteria.
All browser-facing methods are implemented in [client.js](core/client.js).
Most process-related methods are implemented in [core.js](core/core.js).
Also, even though any browser-side code is sandboxed, it is allowed to access browser local storage by going through Tilde Friends API, because sometimes that is useful.
Higher-level behaviors are often implemented within library-style apps
themselves and are beyond the scope of this document.
## Example 3: Files
### Terminal
Suppose you don't want to create your entire app in a single server-side file as we've done with the previous examples. There are some tools to allow you to begin to organize.
All interaction with a human user is through a terminal-like interface. Though
it is somewhat limiting, it makes simple things easy, and it is possible to
construct complicated interfaces by creating and interacting with an iframe.
### app.js
#### terminal.print(arguments...)
```
async function main() {
let html = utf8Decode(await getFile('index.html'));
app.setDocument(html);
}
Print to the terminal. Arguments and lists are recursively expanded. Numerous
special values are supported as implemented in client.cs.
main();
```
// Create a link.
terminal.print({href: "http://www.tildefriends.net/", value: "Tilde Friends!"});
### index.html
// Create an iframe.
terminal.print({iframe: "&lt;b&gt;Hello, world!&lt;/b&gt;", width: 640, height: 480});
```
<html>
<head>
<script type="module" src="script.js"></script>
</head>
<body style="color: #fff">
<h1>File Test</h1>
</body>
</html>
```
// Use style.
terminal.print({style: "color: #f00", value: "Hello, world!"});
### script.js
// Create a link that when clicked will act as if the user typed a command.
terminal.print({command: "exit", value: "Get out of here."});
```
window.addEventListener('load', function() {
document.body.appendChild(document.createTextNode('Hello, world');
});
```
#### terminal.clear()
### Output
Clears the terminal output.
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>File Test</h1>Hello, world!&lt;/body&gt;"></iframe>
#### terminal.readLine()
### Explanation
Read a line of input from the user.
On the server, `utf8Decode(await getFile(fileName))` lets you load a file from your app. In the browser, your app files are made available by HTTP, so you can `<script src="my_script.js"></script>` and such to access them.
#### terminal.setEcho(echo)
## Example 4: Remote Procedure Call
Controls whether the terminal will automatically echo user input. Defaults to true.
While making calls between the client and the server, it is possible to pass functions across that boundary. `tfrpc.js` is a tiny script which builds on that feature to try to hide some of the complexities.
#### terminal.setPrompt(prompt)
### app.js
Sets the terminal prompt. The default is "&gt;".
```
import * as tf from '/tfrpc.js';
#### terminal.setTitle(title)
function sum() {
let s = 0
for (let x of arguments) {
s += x;
}
return s;
}
tf.register(sum);
Sets the browser window/tab title.
async function main() {
app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
```
#### terminal.split(terminalList)
### index.html
Reconfigures the terminal layout, potentially into multiple split panes.
```
<html>
<body>
<h1 id='result'>Calculating...</h1>
</body>
<script type="module" src="script.js"></script>
</html>
```
terminal.split([
{
type: "horizontal",
children: [
{name: "left", basis: "2in", grow: 0, shrink: 0},
{name: "middle", grow: 1},
{name: "right", basis: "2in", grow: 0, shrink: 0},
],
},
]);
### script.js
#### terminal.select(name)
```
import * as tf from '/static/tfrpc.js';
Directs subsequent output to the named terminal.
window.addEventListener('load', async function() {
document.getElementById('result').innerText = await tf.rpc.sum(1, 2, 3);
});
```
#### terminal.postMessageToIframe(iframeName, message)
### Output
Sends a message to the iframe that was created with the given name, using the
browser's window.postMessage.
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>6</h1>&lt;/body&gt;"></iframe>
### Database
### Explanation
Tilde Friends uses lmdb as a basic key value store. Keys and values are all
expected to be of type String. Each application gets its own isolated
database.
Here the browser makes an asynchronous call to the server to do some basic math and update its DOM with the result.
#### database.get(key)
With your favorite Vue/Lit/React/... library on the client-side and your favorite Tilde Friends API calls registered with tfrpc, it becomes pretty easy to start extracting interesting information from, say, SQL queries over Secure Scuttlebutt data, and generating complicated, dynamic user interface. These are the building blocks I used to make the current Tilde Friends SSB client interface.
Retrieve the database value associated with the given key.
## Conclusion
#### database.set(key, value)
Tilde Friends is currently a pile of all the parts that I thought I needed to build interesting web applications, tied together by code that tries to walk the fine line between being secure enough to let us safely run code on the same device and being usable enough that you can open a tab in your browser and start building just by typing code.
Sets the database value for the given key, overwriting any existing value.
#### database.remove(key)
Remove the database entry for the given key.
#### database.getAlll()
Retrieve a list of all key names.
### Network
Network access is generally not extended to untrusted users.
It is necessary to grant network permissions to an app owner through the
administration app.
Apps that require network access must declare it like this:
//! { "permissions": ["network"] }
#### network.newConnection()
Creates a Connection object.
#### connection.connect(host, port)
Opens a TCP connection to host:port.
#### connection.read(readCallback)
Begins reading and calls readCallback(data) for all data received.
#### connection.write(data)
Writes data to the connection.
#### connection.close()
Closes the connection.
I don't claim it thoroughly accomplishes either yet, but I believe it is at a stage where it is showing how promising this approach can be, and I am excited for you to take it for a spin and share.

19
docs/release.md Normal file

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

64
docs/vision.md Normal file

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

@ -0,0 +1,11 @@
* Fix WebView localStorage on some Android versions.
* Fix viewing apps by blob URL.
* Let admin users use the server identity.
* Time out SHS connections that don't handshake.
* Add an SQL index on message type.
* Build an AppImage.
* Improve vote layout.
* Updated CodeMirror to latest.
* Updated OpenSSL to 3.3.2.
* Updated libuv to 1.49.0.
* Updated c-ares to 1.33.1.

@ -0,0 +1,19 @@
* Command-line publishing.
* SSB replication improvements.
* Disallow rich text paste more.
* identity app lets you change the server account.
* SSB profile icons stretching.
* Fixed iPhone app missing data.
* Added an initiate sync button.
* Fixed the AppImage.
* Flatpak WIP.
* Android compatibility fixes.
* Updates:
* CodeMirror
* CommonMark 0.31.2
* Lit 3.2.1
* OpenSSL 3.4.0
* c-ares 1.34.2
* libbacktrace
* libuv 1.49.2
* sqlite 3.47.0

4
package-lock.json generated

@ -11,7 +11,9 @@
}
},
"node_modules/prettier": {
"version": "3.2.5",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

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

@ -11,6 +11,7 @@ import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.FileObserver;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
@ -42,12 +43,6 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStream;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit;
public class TildeFriendsActivity extends Activity {
@ -55,9 +50,9 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsWebView web_view;
String base_url;
String port_file_path;
Thread thread;
Thread server_thread;
ServiceConnection service_connection;
FileObserver observer;
private ValueCallback<Uri[]> upload_message;
private final static int FILECHOOSER_RESULT = 1;
@ -95,56 +90,9 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsActivity activity = this;
thread = new Thread(new Runnable() {
@Override
public void run() {
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Paths.get(getFilesDir().toString()).register(
watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
WatchKey key = watcher.poll(100, TimeUnit.MILLISECONDS);
boolean attempt_it = true;
if (key != null)
{
attempt_it = false;
for (WatchEvent event : key.pollEvents()) {
if (event.context().toString().equals("port.txt")) {
Log.w("tildefriends", "Observed file write: " + event.context().toString());
attempt_it = true;
}
}
}
if (attempt_it) {
int port = read_port(port_file_path);
if (port >= 0) {
base_url = "http://127.0.0.1:" + String.valueOf(port) + "/";
activity.runOnUiThread(() -> {
activity.hide_status();
web_view.loadUrl(base_url);
});
break;
} else {
activity.runOnUiThread(() -> {
activity.set_status("Waiting to connect...");
});
}
}
if (key != null && !key.reset()) {
Log.w("tildefriends", "watcher is no longer valid");
break;
}
}
} catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e.toString());
} catch (InterruptedException e) {
Log.w("tildefriends", "InterruptedException: " + e.toString());
}
}
});
thread.start();
Log.w("tildefriends", "Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
set_status("Starting server...");
server_thread = new Thread(new Runnable() {
@ -165,6 +113,8 @@ public class TildeFriendsActivity extends Activity {
web_view.getSettings().setDatabaseEnabled(true);
web_view.getSettings().setDomStorageEnabled(true);
set_database_path();
web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) {
Log.w("tildefriends", "Let's download: " + url + " (" + content_disposition + ")");
@ -390,11 +340,14 @@ public class TildeFriendsActivity extends Activity {
private int read_port(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return Integer.parseInt(reader.readLine());
String line = reader.readLine();
if (line != null) {
return Integer.parseInt(line);
}
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (java.io.FileNotFoundException e) {
e.printStackTrace();
Log.w("tildefriends", "Port file does not yet exist.");
} catch (java.io.IOException e) {
e.printStackTrace();
}
@ -458,4 +411,42 @@ public class TildeFriendsActivity extends Activity {
s_activity.service_connection = null;
}
}
private void check_port_file(String path) {
int port = read_port(port_file_path);
if (port >= 0) {
base_url = "http://127.0.0.1:" + String.valueOf(port) + "/";
runOnUiThread(() -> {
hide_status();
web_view.loadUrl(base_url);
});
observer = null;
} else {
runOnUiThread(() -> {
set_status("Waiting to connect...");
});
}
}
@SuppressWarnings("deprecation")
private FileObserver make_file_observer(String dir, String path) {
FileObserver file_observer = new FileObserver(dir, FileObserver.ALL_EVENTS) {
@Override
public void onEvent(int event, String file) {
if (observer != null) {
check_port_file(path);
}
}
};
check_port_file(path);
return file_observer;
}
@SuppressWarnings("deprecation")
private void set_database_path()
{
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
web_view.getSettings().setDatabasePath(getDatabasePath("webview").getPath());
}
}
}

@ -41,7 +41,7 @@ public class TildeFriendsSandboxService extends Service {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
if (code == START_CALL) {
ParcelFileDescriptor pfd = data.readParcelable(ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class);
ParcelFileDescriptor pfd = read_pfd(data);
if (pfd != null) {
Log.w("tildefriends", "fd is " + pfd.getFd());
start_thread(pfd.detachFd());
@ -56,4 +56,9 @@ public class TildeFriendsSandboxService extends Service {
}
};
}
@SuppressWarnings("deprecation")
static private ParcelFileDescriptor read_pfd(Parcel data) {
return data.readParcelable(ParcelFileDescriptor.class.getClassLoader());
}
}

@ -0,0 +1,26 @@
id: com.unprompted.tildefriends
runtime: org.freedesktop.Platform
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
command: tildefriends-run.sh
finish-args:
- --share=network
- --filesystem=xdg-data/applications/tildefriends
modules:
- name: tildefriends
buildsystem: simple
build-commands:
- make release out/data.zip
- install -Dm755 out/release/tildefriends /app/bin/tildefriends
- install -D out/data.zip /app/share/data.zip
- install -Dm755 tildefriends-run.sh /app/bin/tildefriends-run.sh
sources:
- type: git
url: https://dev.tildefriends.net/cory/tildefriends.git
dest: .
commit: main
- type: script
dest-filename: tildefriends-run.sh
commands:
- mkdir -p ~/.local/share/applications/tildefriends/
- exec tildefriends run -z /app/share/data.zip -d ~/.local/share/applications/tildefriends/db.sqlite

@ -390,8 +390,6 @@ static JSValue _database_remove(JSContext* context, JSValueConst this_val, int a
typedef struct _database_get_all_t
{
const char* id;
const char* key;
size_t key_length;
char** out_values;
size_t* out_lengths;
int out_values_length;
@ -455,17 +453,11 @@ static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int
{
tf_ssb_t* ssb = tf_task_get_ssb(database->task);
size_t length;
const char* key = JS_ToCStringLen(context, &length, argv[0]);
database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1 + length + 1);
database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1);
*work = (database_get_all_t) {
.id = (const char*)(work + 1),
.key = (const char*)(work + 1) + strlen(database->id) + 1,
.key_length = length,
};
memcpy((char*)work->id, database->id, strlen(database->id) + 1);
memcpy((char*)work->key, key, length + 1);
JS_FreeCString(context, key);
tf_ssb_run_work(ssb, _database_get_all_work, _database_get_all_after_work, work);
result = JS_NewPromiseCapability(context, work->promise);

@ -107,6 +107,7 @@ static const char* _http_connection_get_header(const tf_http_connection_t* conne
static void _http_connection_destroy(tf_http_connection_t* connection, const char* reason);
static void _http_timer_reset(tf_http_connection_t* connection);
static void _http_tls_update(tf_http_connection_t* connection);
static void _http_builtin_404_handler(tf_http_request_t* request);
tf_http_t* tf_http_create(uv_loop_t* loop)
{
@ -196,6 +197,10 @@ static void _http_connection_on_close(uv_handle_t* handle)
static void _http_request_destroy(tf_http_request_t* request)
{
tf_http_close_callback* on_close = request->on_close;
if (request->connection && !request->connection->is_response_sent)
{
_http_builtin_404_handler(request);
}
if (on_close)
{
tf_trace_t* trace = request->http->trace;
@ -402,6 +407,11 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
if (connection->body_length == connection->content_length)
{
/* Null-terminate for convenience. */
if (connection->body)
{
((char*)connection->body)[connection->body_length] = '\0';
}
tf_http_request_t* request = tf_malloc(sizeof(tf_http_request_t));
*request = (tf_http_request_t) {
.http = connection->http,
@ -495,7 +505,7 @@ static size_t _http_on_read_plain_internal(tf_http_connection_t* connection, con
if (connection->content_length)
{
connection->body = tf_realloc(connection->body, connection->content_length);
connection->body = tf_realloc(connection->body, connection->content_length + 1);
}
if (!_http_find_handler(connection->http, connection->path, &connection->callback, &connection->trace_name, &connection->user_data) || !connection->callback)

@ -4,8 +4,8 @@
#include "http.h"
#include "log.h"
#include "mem.h"
#include "ssb.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "tlscontext.js.h"
#include "trace.h"
@ -39,8 +39,11 @@ const int64_t k_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static bool _is_name_valid(const char* name);
static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
const char** _form_data_decode(const char* data, int length);
const char* _form_data_get(const char** form_data, const char* key);
static JSClassID _httpd_class_id;
static JSClassID _httpd_request_class_id;
@ -555,14 +558,11 @@ static bool _magic_bytes_match(const magic_bytes_t* magic, const uint8_t* actual
return true;
}
static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
static const char* _httpd_mime_type_from_magic_bytes_internal(const uint8_t* bytes, size_t size)
{
JSValue result = JS_UNDEFINED;
size_t size = 0;
uint8_t* bytes = tf_util_try_get_array_buffer(context, &size, argv[0]);
const char* type = "application/binary";
if (bytes)
{
const magic_bytes_t k_magic_bytes[] = {
{
.type = "image/jpeg",
@ -627,12 +627,19 @@ static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueCons
{
if (_magic_bytes_match(&k_magic_bytes[i], bytes, size))
{
result = JS_NewString(context, k_magic_bytes[i].type);
type = k_magic_bytes[i].type;
break;
}
}
}
return result;
return type;
}
static JSValue _httpd_mime_type_from_magic_bytes(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
size_t size = 0;
uint8_t* bytes = tf_util_try_get_array_buffer(context, &size, argv[0]);
return JS_NewString(context, _httpd_mime_type_from_magic_bytes_internal(bytes, size));
}
static const char* _ext_to_content_type(const char* ext, bool use_fallback)
@ -959,6 +966,410 @@ static void _httpd_endpoint_static(tf_http_request_t* request)
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}
typedef struct _user_app_t
{
const char* user;
const char* app;
} user_app_t;
static user_app_t* _parse_user_app_from_path(const char* path, const char* expected_suffix)
{
if (!path || path[0] != '/' || path[1] != '~')
{
return NULL;
}
size_t length = strlen(path);
size_t suffix_length = expected_suffix ? strlen(expected_suffix) : 0;
if (length < suffix_length || strcmp(path + length - suffix_length, expected_suffix) != 0)
{
return NULL;
}
const char* slash = strchr(path + 2, '/');
if (!slash)
{
return NULL;
}
const char* user = path + 2;
size_t user_length = (size_t)(slash - user);
const char* app = slash + 1;
size_t app_length = (size_t)(length - suffix_length - user_length - 3);
user_app_t* result = tf_malloc(sizeof(user_app_t) + user_length + 1 + app_length + 1);
*result = (user_app_t) {
.user = (char*)(result + 1),
.app = (char*)(result + 1) + user_length + 1,
};
memcpy((char*)result->user, user, user_length);
((char*)result->user)[user_length] = '\0';
memcpy((char*)result->app, app, app_length);
((char*)result->app)[app_length] = '\0';
if (!_is_name_valid(result->user) || !_is_name_valid(result->app))
{
tf_free(result);
result = NULL;
}
return result;
}
typedef struct _view_t
{
tf_http_request_t* request;
const char** form_data;
void* data;
size_t size;
bool not_modified;
} view_t;
static bool _is_filename_safe(const char* filename)
{
if (!filename)
{
return NULL;
}
for (const char* p = filename; *p; p++)
{
if ((*p <= 'a' && *p >= 'z') && (*p <= 'A' && *p >= 'Z') && (*p <= '0' && *p >= '9') && *p != '.' && *p != '-' && *p != '_')
{
return false;
}
}
return strlen(filename) < 256;
}
static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data)
{
view_t* view = user_data;
tf_http_request_t* request = view->request;
char blob_id[256] = "";
user_app_t* user_app = _parse_user_app_from_path(request->path, "/view");
if (user_app)
{
size_t app_path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(app_path_length);
snprintf(app_path, app_path_length, "path:%s", user_app->app);
const char* value = tf_ssb_db_get_property(ssb, user_app->user, app_path);
snprintf(blob_id, sizeof(blob_id), "%s", value);
tf_free(app_path);
tf_free((void*)value);
}
else if (request->path[0] == '/' && request->path[1] == '&')
{
snprintf(blob_id, sizeof(blob_id), "%.*s", (int)(strlen(request->path) - strlen("/view") - 1), request->path + 1);
}
tf_free(user_app);
if (*blob_id)
{
const char* if_none_match = tf_http_request_get_header(request, "if-none-match");
char match[258];
snprintf(match, sizeof(match), "\"%s\"", blob_id);
if (if_none_match && strcmp(if_none_match, match))
{
view->not_modified = true;
}
else
{
tf_ssb_db_blob_get(ssb, blob_id, (uint8_t**)&view->data, &view->size);
}
}
}
static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
view_t* view = user_data;
const char* filename = _form_data_get(view->form_data, "filename");
if (!_is_filename_safe(filename))
{
filename = NULL;
}
char content_disposition[512] = "";
if (filename)
{
snprintf(content_disposition, sizeof(content_disposition), "attachment; filename=%s", filename);
}
const char* headers[] = {
"Content-Security-Policy",
"sandbox allow-downloads allow-top-navigation-by-user-activation",
"Content-Type",
view->data ? _httpd_mime_type_from_magic_bytes_internal(view->data, view->size) : "text/plain",
filename ? "Content-Disposition" : NULL,
filename ? content_disposition : NULL,
};
int count = filename ? tf_countof(headers) / 2 : (tf_countof(headers) / 2 - 1);
if (view->not_modified)
{
tf_http_respond(view->request, 304, headers, count, NULL, 0);
}
else if (view->data)
{
tf_http_respond(view->request, 200, headers, count, view->data, view->size);
tf_free(view->data);
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(view->request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_free(view->form_data);
tf_http_request_unref(view->request);
tf_free(view);
}
static void _httpd_endpoint_view(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
view_t* view = tf_malloc(sizeof(view_t));
*view = (view_t) { .request = request, .form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0) };
tf_ssb_run_work(ssb, _httpd_endpoint_view_work, _httpd_endpoint_view_after_work, view);
}
typedef struct _save_t
{
tf_http_request_t* request;
int response;
char blob_id[256];
} save_t;
static void _httpd_endpoint_save_work(tf_ssb_t* ssb, void* user_data)
{
save_t* save = user_data;
tf_http_request_t* request = save->request;
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue jwt = _authenticate_jwt(ssb, context, session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && _is_name_valid(user_string))
{
user_app_t* user_app = _parse_user_app_from_path(request->path, "/save");
if (user_app)
{
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration")))
{
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", user_app->app);
const char* old_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path);
JSValue new_app = JS_ParseJSON(context, request->body, request->content_length, NULL);
tf_util_report_error(context, new_app);
if (JS_IsObject(new_app))
{
uint8_t* old_blob = NULL;
size_t old_blob_size = 0;
if (tf_ssb_db_blob_get(ssb, old_blob_id, &old_blob, &old_blob_size))
{
JSValue old_app = JS_ParseJSON(context, (const char*)old_blob, old_blob_size, NULL);
if (JS_IsObject(old_app))
{
JSAtom previous = JS_NewAtom(context, "previous");
JS_DeleteProperty(context, old_app, previous, 0);
JS_DeleteProperty(context, new_app, previous, 0);
JSValue old_app_json = JS_JSONStringify(context, old_app, JS_NULL, JS_NULL);
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
const char* old_app_str = JS_ToCString(context, old_app_json);
const char* new_app_str = JS_ToCString(context, new_app_json);
if (old_app_str && new_app_str && strcmp(old_app_str, new_app_str) == 0)
{
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", old_blob_id);
save->response = 200;
}
JS_FreeCString(context, old_app_str);
JS_FreeCString(context, new_app_str);
JS_FreeValue(context, old_app_json);
JS_FreeValue(context, new_app_json);
JS_FreeAtom(context, previous);
}
JS_FreeValue(context, old_app);
tf_free(old_blob);
}
if (!save->response)
{
if (old_blob_id)
{
JS_SetPropertyStr(context, new_app, "previous", JS_NewString(context, old_blob_id));
}
JSValue new_app_json = JS_JSONStringify(context, new_app, JS_NULL, JS_NULL);
size_t new_app_length = 0;
const char* new_app_str = JS_ToCStringLen(context, &new_app_length, new_app_json);
char blob_id[250] = { 0 };
if (tf_ssb_db_blob_store(ssb, (const uint8_t*)new_app_str, new_app_length, blob_id, sizeof(blob_id), NULL) &&
tf_ssb_db_set_property(ssb, user_app->user, app_path, blob_id))
{
tf_ssb_db_add_value_to_array_property(ssb, user_app->user, "apps", user_app->app);
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
save->response = 200;
}
JS_FreeCString(context, new_app_str);
JS_FreeValue(context, new_app_json);
}
}
else
{
save->response = 400;
}
JS_FreeValue(context, new_app);
tf_free(app_path);
tf_free((void*)old_blob_id);
}
else
{
save->response = 401;
}
tf_free(user_app);
}
else if (strcmp(request->path, "/save") == 0)
{
char blob_id[250] = { 0 };
if (tf_ssb_db_blob_store(ssb, request->body, request->content_length, blob_id, sizeof(blob_id), NULL))
{
snprintf(save->blob_id, sizeof(save->blob_id), "/%s", blob_id);
save->response = 200;
}
}
else
{
save->response = 400;
}
}
tf_free((void*)session);
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_save_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
save_t* save = user_data;
tf_http_request_t* request = save->request;
if (*save->blob_id)
{
tf_http_respond(request, 200, NULL, 0, save->blob_id, strlen(save->blob_id));
}
tf_http_request_unref(request);
tf_free(save);
}
static void _httpd_endpoint_save(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
save_t* save = tf_malloc(sizeof(save_t));
*save = (save_t) {
.request = request,
};
tf_ssb_run_work(ssb, _httpd_endpoint_save_work, _httpd_endpoint_save_after_work, save);
}
typedef struct _delete_t
{
tf_http_request_t* request;
const char* session;
int response;
} delete_t;
static void _httpd_endpoint_delete_work(tf_ssb_t* ssb, void* user_data)
{
delete_t* delete = user_data;
tf_http_request_t* request = delete->request;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue jwt = _authenticate_jwt(ssb, context, delete->session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && _is_name_valid(user_string))
{
user_app_t* user_app = _parse_user_app_from_path(request->path, "/delete");
if (user_app)
{
if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, user_string, "administration")))
{
size_t path_length = strlen("path:") + strlen(user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", user_app->app);
bool changed = false;
changed = tf_ssb_db_remove_value_from_array_property(ssb, user_string, "apps", user_app->app) || changed;
changed = tf_ssb_db_remove_property(ssb, user_string, app_path) || changed;
delete->response = changed ? 200 : 404;
tf_free(app_path);
}
else
{
delete->response = 401;
}
}
else
{
delete->response = 404;
}
tf_free(user_app);
}
else
{
delete->response = 401;
}
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_delete_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
delete_t* delete = user_data;
const char* k_payload = tf_http_status_text(delete->response ? delete->response : 404);
tf_http_respond(delete->request, delete->response ? delete->response : 404, NULL, 0, k_payload, strlen(k_payload));
tf_http_request_unref(delete->request);
tf_free((void*)delete->session);
tf_free(delete);
}
static void _httpd_endpoint_delete(tf_http_request_t* request)
{
tf_http_request_ref(request);
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
delete_t* delete = tf_malloc(sizeof(delete_t));
*delete = (delete_t) {
.request = request,
.session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session"),
};
tf_ssb_run_work(ssb, _httpd_endpoint_delete_work, _httpd_endpoint_delete_after_work, delete);
}
static void _httpd_endpoint_root_callback(const char* path, void* user_data)
{
tf_http_request_t* request = user_data;
@ -1280,7 +1691,7 @@ static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char*
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
{
bool result = false;
JSValue user = JS_GetPropertyStr(context, session, "user");
JSValue user = JS_GetPropertyStr(context, session, "name");
const char* user_string = JS_ToCString(context, user);
result = user_string && strcmp(user_string, "guest") != 0;
JS_FreeCString(context, user_string);
@ -1689,6 +2100,11 @@ void tf_httpd_register(JSContext* context)
tf_http_add_handler(http, "/static/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/.well-known/*", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task);
tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task);
tf_http_add_handler(http, "/~*/*/save", _httpd_endpoint_save, NULL, task);
tf_http_add_handler(http, "/~*/*/delete", _httpd_endpoint_delete, NULL, task);
tf_http_add_handler(http, "/save", _httpd_endpoint_save, NULL, task);
tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL);
tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);

Binary file not shown.

@ -48,11 +48,12 @@ struct backtrace_state* g_backtrace_state;
const char* k_db_path_default = "db.sqlite";
#if !TARGET_OS_IPHONE && !defined(__ANDROID__)
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_export(const char* file, int argc, char* argv[]);
static int _tf_command_import(const char* file, int argc, char* argv[]);
static int _tf_command_publish(const char* file, int argc, char* argv[]);
static int _tf_command_run(const char* file, int argc, char* argv[]);
static int _tf_command_sandbox(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_usage(const char* file);
@ -68,6 +69,7 @@ const command_t k_commands[] = {
{ "sandbox", _tf_command_sandbox, "Run a sandboxed tildefriends sandbox process (used internally)." },
{ "import", _tf_command_import, "Import apps to SSB." },
{ "export", _tf_command_export, "Export apps from SSB." },
{ "publish", _tf_command_publish, "Append a message to a feed." },
{ "verify", _tf_command_verify, "Verify a feed." },
{ "test", _tf_command_test, "Test SSB." },
};
@ -274,6 +276,121 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
return EXIT_SUCCESS;
}
static void _tf_published_callback(const char* id, bool verified, bool stored, void* user_data)
{
if (verified)
{
if (stored)
{
tf_printf("Message %s stored.\n", id);
}
else
{
tf_printf("Unable to store the message.\n");
}
}
else
{
tf_printf("Failed to verify the message.\n");
}
*(int*)user_data = stored ? EXIT_SUCCESS : EXIT_FAILURE;
}
static int _tf_command_publish(const char* file, int argc, char* argv[])
{
const char* user = NULL;
const char* identity = NULL;
const char* db_path = k_db_path_default;
const char* content = NULL;
bool show_usage = false;
while (!show_usage)
{
static const struct option k_options[] = {
{ "user", required_argument, NULL, 'u' },
{ "id", required_argument, NULL, 'i' },
{ "db-path", required_argument, NULL, 'd' },
{ "content", required_argument, NULL, 'c' },
{ "help", no_argument, NULL, 'h' },
{ 0 },
};
int c = getopt_long(argc, argv, "u:i:d:c:h", k_options, NULL);
if (c == -1)
{
break;
}
switch (c)
{
case '?':
case 'h':
default:
show_usage = true;
break;
case 'u':
user = optarg;
break;
case 'i':
identity = optarg;
break;
case 'd':
db_path = optarg;
break;
case 'c':
content = optarg;
break;
}
}
if (show_usage || !user || !identity || !content)
{
tf_printf("\n%s publish [options]\n\n", file);
tf_printf("options:\n");
tf_printf(" -y, --user user User owning identity with which to publish.\n");
tf_printf(" -i, --identity 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(" -c, --content json JSON content of message to publish.\n");
tf_printf(" -h, --help Show this usage information.\n");
return EXIT_FAILURE;
}
int result = EXIT_FAILURE;
tf_printf("Posting %s as account %s belonging to %s...\n", content, identity, user);
tf_ssb_t* ssb = tf_ssb_create(NULL, NULL, db_path, NULL);
uint8_t private_key[512] = { 0 };
if (tf_ssb_db_identity_get_private_key(ssb, user, identity, private_key, sizeof(private_key)))
{
JSContext* context = tf_ssb_get_context(ssb);
int64_t sequence = 0;
char previous[k_id_base64_len] = { 0 };
tf_ssb_db_get_latest_message_by_author(ssb, identity, &sequence, previous, sizeof(previous));
JSValue content_value = JS_ParseJSON(context, content, strlen(content), NULL);
if (!JS_IsException(content_value))
{
JSValue message = tf_ssb_sign_message(ssb, identity, private_key, content_value, previous, sequence);
JSValue message_value = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
const char* message_str = JS_ToCString(context, message_value);
tf_printf("Posting: %s.\n", message_str);
tf_ssb_verify_strip_and_store_message(ssb, message, _tf_published_callback, &result);
JS_FreeCString(context, message_str);
JS_FreeValue(context, message_value);
JS_FreeValue(context, message);
}
else
{
tf_printf("Unable to parse content as JSON: ");
tf_util_report_error(context, content_value);
}
JS_FreeValue(context, content_value);
}
else
{
tf_printf("Did not find private key for identity %s belonging to %s.\n", identity, user);
}
tf_ssb_destroy(ssb);
return result;
}
static int _tf_command_verify(const char* file, int argc, char* argv[])
{
const char* identity = NULL;
@ -283,7 +400,7 @@ static int _tf_command_verify(const char* file, int argc, char* argv[])
while (!show_usage)
{
static const struct option k_options[] = {
{ "id", required_argument, NULL, 'u' },
{ "id", required_argument, NULL, 'i' },
{ "db-path", required_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ 0 },

@ -2,15 +2,14 @@
#include "util.js.h"
#include "uv.h"
#include "quickjs.h"
#include "sqlite3.h"
#include "uv.h"
#include <openssl/crypto.h>
#include <string.h>
#include <stdbool.h>
#include <string.h>
typedef struct _tf_mem_node_t tf_mem_node_t;

@ -6,9 +6,9 @@
#include "trace.h"
#include "util.js.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>

416
src/ssb.c

@ -80,6 +80,8 @@ enum
k_seed_expire_seconds = 10 * 60,
k_seed_check_interval_seconds = 5 * 60,
k_udp_discovery_expires_seconds = 10,
k_handshake_timeout_ms = 15000,
k_rpc_active_ms = 3000,
};
typedef struct _tf_ssb_broadcast_t tf_ssb_broadcast_t;
@ -111,6 +113,7 @@ typedef struct _tf_ssb_request_t
tf_ssb_callback_cleanup_t* cleanup;
void* user_data;
tf_ssb_connection_t* dependent_connection;
uint64_t last_active;
int32_t request_number;
} tf_ssb_request_t;
@ -130,8 +133,7 @@ typedef struct _tf_ssb_broadcast_t
typedef struct _tf_ssb_rpc_callback_node_t tf_ssb_rpc_callback_node_t;
typedef struct _tf_ssb_rpc_callback_node_t
{
const char** name;
const char* flattened_name;
const char* name;
tf_ssb_rpc_callback_t* callback;
tf_ssb_callback_cleanup_t* cleanup;
void* user_data;
@ -211,6 +213,7 @@ typedef struct _tf_ssb_t
uv_timer_t broadcast_cleanup_timer;
uv_timer_t broadcast_timer;
uv_timer_t trace_timer;
uv_timer_t request_activity_timer;
uv_tcp_t server;
uint8_t network_key[32];
@ -294,6 +297,7 @@ typedef struct _tf_ssb_connection_t
uv_tcp_t tcp;
uv_connect_t connect;
uv_async_t async;
uv_timer_t handshake_timer;
bool closing;
tf_ssb_connection_t* tunnel_connection;
@ -359,6 +363,10 @@ typedef struct _tf_ssb_connection_t
int read_back_pressure;
int active_write_count;
uint64_t last_notified_active;
int flags;
} tf_ssb_connection_t;
static JSClassID _connection_class_id;
@ -656,7 +664,42 @@ static int _request_compare(const void* a, const void* b)
return ai < br->request_number ? -1 : br->request_number < ai ? 1 : 0;
}
static bool _tf_ssb_connection_get_request_callback(tf_ssb_connection_t* connection, int32_t request_number, tf_ssb_rpc_callback_t** out_callback, void** out_user_data)
static void _tf_ssb_request_activity_timer(uv_timer_t* timer)
{
tf_ssb_t* ssb = timer->data;
uint64_t now_ms = uv_now(ssb->loop);
bool any_still_active = false;
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
{
bool any_changed = false;
bool last_notified_active = (now_ms - connection->last_notified_active) < k_rpc_active_ms;
for (int i = 0; i < connection->requests_count; i++)
{
bool last_active = (now_ms - connection->requests[i].last_active) < k_rpc_active_ms;
if (last_active != last_notified_active)
{
any_changed = true;
}
if (last_active)
{
any_still_active = true;
}
}
if (any_changed)
{
_tf_ssb_notify_connections_changed(ssb, k_tf_ssb_change_update, connection);
connection->last_notified_active = now_ms;
}
}
if (any_still_active && uv_timer_get_due_in(&ssb->request_activity_timer) == 0)
{
uv_timer_start(&ssb->request_activity_timer, _tf_ssb_request_activity_timer, k_rpc_active_ms, 0);
}
}
static bool _tf_ssb_connection_get_request_callback(
tf_ssb_connection_t* connection, int32_t request_number, tf_ssb_rpc_callback_t** out_callback, void** out_user_data, const char** out_name)
{
if (!connection->requests)
{
@ -673,6 +716,15 @@ static bool _tf_ssb_connection_get_request_callback(tf_ssb_connection_t* connect
{
*out_user_data = request->user_data;
}
if (out_name)
{
*out_name = request->name;
}
request->last_active = uv_now(connection->ssb->loop);
if (uv_timer_get_due_in(&connection->ssb->request_activity_timer) == 0)
{
uv_timer_start(&connection->ssb->request_activity_timer, _tf_ssb_request_activity_timer, k_rpc_active_ms, 0);
}
return true;
}
return false;
@ -683,12 +735,14 @@ void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t requ
{
tf_ssb_request_t* existing =
connection->requests_count ? bsearch(&request_number, connection->requests, connection->requests_count, sizeof(tf_ssb_request_t), _request_compare) : NULL;
uint64_t now_ms = uv_now(connection->ssb->loop);
if (existing)
{
assert(!existing->callback);
assert(!existing->cleanup);
assert(!existing->user_data);
assert(!existing->dependent_connection);
existing->last_active = now_ms;
existing->callback = callback;
existing->cleanup = cleanup;
existing->user_data = user_data;
@ -703,6 +757,7 @@ void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t requ
.cleanup = cleanup,
.user_data = user_data,
.dependent_connection = dependent_connection,
.last_active = now_ms,
};
snprintf(request.name, sizeof(request.name), "%s", name);
int index = tf_util_insert_index(&request_number, connection->requests, connection->requests_count, sizeof(tf_ssb_request_t), _request_compare);
@ -713,10 +768,14 @@ void tf_ssb_connection_add_request(tf_ssb_connection_t* connection, int32_t requ
}
connection->requests[index] = request;
connection->requests_count++;
connection->ssb->request_count++;
}
if (uv_timer_get_due_in(&connection->ssb->request_activity_timer) == 0)
{
uv_timer_start(&connection->ssb->request_activity_timer, _tf_ssb_request_activity_timer, k_rpc_active_ms, 0);
}
_tf_ssb_notify_connections_changed(connection->ssb, k_tf_ssb_change_update, connection);
connection->last_notified_active = now_ms;
}
static int _message_request_compare(const void* a, const void* b)
@ -781,34 +840,35 @@ void tf_ssb_connection_remove_request(tf_ssb_connection_t* connection, int32_t r
}
}
void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, const uint8_t* message, size_t size,
bool tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, const uint8_t* message, size_t size,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data)
{
const char* request_name = "<unknown>";
if (!connection)
{
if (cleanup)
{
cleanup(NULL, user_data);
}
return;
return false;
}
if (flags & k_ssb_rpc_flag_new_request)
{
assert(request_number > 0);
assert(!_tf_ssb_connection_get_request_callback(connection, request_number, NULL, NULL));
assert(!_tf_ssb_connection_get_request_callback(connection, request_number, NULL, NULL, NULL));
assert(new_request_name);
}
else if (!_tf_ssb_connection_get_request_callback(connection, request_number, NULL, NULL))
else if (!_tf_ssb_connection_get_request_callback(connection, request_number, NULL, NULL, &request_name))
{
if (flags & k_ssb_rpc_flag_binary)
{
tf_printf("Dropping message with no active request (%d): (%zd bytes).\n", request_number, size);
tf_printf(MAGENTA "%s RPC DROP" RESET " message with no active request (%d): (%zd bytes).\n", connection->name, request_number, size);
}
else
{
tf_printf("Dropping message with no active request (%d): %.*s\n", request_number, (int)size, message);
tf_printf(MAGENTA "%s RPC DROP" RESET " message with no active request (%d): %.*s\n", connection->name, request_number, (int)size, message);
}
return;
return false;
}
uint8_t* combined = tf_malloc(9 + size);
@ -820,9 +880,9 @@ void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags,
memcpy(combined + 1 + 2 * sizeof(uint32_t), message, size);
if (connection->ssb->verbose)
{
tf_printf(MAGENTA "%s RPC SEND" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B] %.*s\n", connection->name, (flags & k_ssb_rpc_flag_end_error) ? "true" : "false",
(flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type], request_number, size,
(flags & k_ssb_rpc_mask_type) == k_ssb_rpc_flag_binary ? 0 : (int)size, message);
tf_printf(MAGENTA "%s RPC SEND[%s]" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B] %.*s\n", connection->name, request_name,
(flags & k_ssb_rpc_flag_end_error) ? "true" : "false", (flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type],
request_number, size, (flags & k_ssb_rpc_mask_type) == k_ssb_rpc_flag_binary ? 0 : (int)size, message);
}
_tf_ssb_connection_add_debug_message(connection, true, flags & k_ssb_rpc_mask_send, request_number, message, size);
_tf_ssb_connection_box_stream_send(connection, combined, 1 + 2 * sizeof(uint32_t) + size);
@ -836,22 +896,24 @@ void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags,
{
tf_ssb_connection_add_request(connection, request_number, new_request_name, callback, cleanup, user_data, NULL);
}
return true;
}
void tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, JSValue message,
bool tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, JSValue message,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data)
{
JSContext* context = connection->ssb->context;
JSValue json = JS_JSONStringify(context, message, JS_NULL, JS_NULL);
size_t size = 0;
const char* json_string = JS_ToCStringLen(context, &size, json);
tf_ssb_connection_rpc_send(
bool result = tf_ssb_connection_rpc_send(
connection, k_ssb_rpc_flag_json | (flags & ~k_ssb_rpc_mask_type), request_number, new_request_name, (const uint8_t*)json_string, size, callback, cleanup, user_data);
JS_FreeCString(context, json_string);
JS_FreeValue(context, json);
return result;
}
void tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* error)
bool tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* error)
{
JSContext* context = connection->ssb->context;
JSValue message = JS_NewObject(context);
@ -859,17 +921,18 @@ void tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t f
JS_SetPropertyStr(context, message, "name", JS_NewString(context, "Error"));
JS_SetPropertyStr(context, message, "stack", JS_NewString(context, stack ? stack : "stack unavailable"));
JS_SetPropertyStr(context, message, "message", JS_NewString(context, error));
tf_ssb_connection_rpc_send_json(
bool result = tf_ssb_connection_rpc_send_json(
connection, ((flags & k_ssb_rpc_flag_stream) ? (k_ssb_rpc_flag_stream) : 0) | k_ssb_rpc_flag_end_error, request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
tf_free((void*)stack);
return result;
}
void tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* name)
bool tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* name)
{
char buffer[1024];
char buffer[1024] = "";
snprintf(buffer, sizeof(buffer), "method '%s' is not in list of allowed methods", name);
tf_ssb_connection_rpc_send_error(connection, flags, request_number, buffer);
return tf_ssb_connection_rpc_send_error(connection, flags, request_number, buffer);
}
static int _utf8_len(uint8_t ch)
@ -1096,11 +1159,11 @@ bool tf_ssb_verify_and_strip_signature(JSContext* context, JSValue val, char* ou
return false;
}
void tf_ssb_close_all(tf_ssb_t* ssb)
void tf_ssb_close_all(tf_ssb_t* ssb, const char* reason)
{
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
{
_tf_ssb_connection_close(connection, "tf_ssb_close_all");
_tf_ssb_connection_close(connection, reason);
}
}
@ -1258,6 +1321,10 @@ static void _tf_ssb_connection_verify_identity(tf_ssb_connection_t* connection,
JS_SetPropertyStr(context, connection->object, "is_client", JS_TRUE);
connection->state = k_tf_ssb_state_verified;
if (connection->handshake_timer.data)
{
uv_timer_stop(&connection->handshake_timer);
}
_tf_ssb_notify_connections_changed(connection->ssb, k_tf_ssb_change_connect, connection);
}
@ -1486,6 +1553,10 @@ static void _tf_ssb_connection_verify_client_identity(tf_ssb_connection_t* conne
JS_SetPropertyStr(context, connection->object, "is_client", JS_FALSE);
connection->state = k_tf_ssb_state_server_verified;
if (connection->handshake_timer.data)
{
uv_timer_stop(&connection->handshake_timer);
}
_tf_ssb_notify_connections_changed(connection->ssb, k_tf_ssb_change_connect, connection);
}
@ -1511,60 +1582,28 @@ static bool _tf_ssb_connection_recv_pop(tf_ssb_connection_t* connection, uint8_t
return true;
}
static bool _tf_ssb_name_equals(JSContext* context, JSValue object, const char** match)
static void _tf_ssb_name_to_string(JSContext* context, JSValue object, char* buffer, size_t size)
{
bool result = true;
JSValue name = JS_GetPropertyStr(context, object, "name");
if (JS_IsArray(context, name))
{
int length = tf_util_get_length(context, name);
int offset = 0;
for (int i = 0; i < length; i++)
{
if (!match[i])
{
result = false;
break;
}
JSValue element = JS_GetPropertyUint32(context, name, i);
const char* str = JS_ToCString(context, element);
if (!str || strcmp(str, match[i]) != 0)
{
result = false;
}
JS_FreeCString(context, str);
JS_FreeValue(context, element);
}
if (result && match[length])
{
result = false;
JSValue part = JS_GetPropertyUint32(context, name, i);
const char* part_str = JS_ToCString(context, part);
offset += snprintf(buffer + offset, size - offset, "%s%s", i == 0 ? "" : ".", part_str);
JS_FreeCString(context, part_str);
JS_FreeValue(context, part);
}
}
else if (JS_IsString(name))
{
/* Manifest is traditionally sent as not an array for some reason. */
const char* str = JS_ToCString(context, name);
result = str && match[0] && strcmp(str, match[0]) == 0 && !match[1];
JS_FreeCString(context, str);
const char* part_str = JS_ToCString(context, name);
snprintf(buffer, size, "%s", part_str);
JS_FreeCString(context, part_str);
}
else
{
result = false;
}
JS_FreeValue(context, name);
return result;
}
static void _tf_ssb_name_to_string(JSContext* context, JSValue object, char* buffer, size_t size)
{
JSValue name = JS_GetPropertyStr(context, object, "name");
JSValue json_val = JS_JSONStringify(context, name, JS_NULL, JS_NewInt32(context, 2));
const char* value = JS_ToCString(context, json_val);
snprintf(buffer, size, "%s", value);
JS_FreeCString(context, value);
JS_FreeValue(context, json_val);
JS_FreeValue(context, name);
}
@ -1580,21 +1619,22 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
}
else if (flags & k_ssb_rpc_flag_json)
{
char id[k_id_base64_len] = "";
tf_ssb_id_bin_to_str(id, sizeof(id), connection->serverpub);
tf_ssb_rpc_callback_t* callback = NULL;
void* user_data = NULL;
const char* request_name = "<unknown>";
bool have_request = _tf_ssb_connection_get_request_callback(connection, -request_number, &callback, &user_data, &request_name);
if (connection->ssb->verbose)
{
tf_printf(CYAN "%s RPC RECV" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B] %.*s\n", connection->name, (flags & k_ssb_rpc_flag_end_error) ? "true" : "false",
(flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type], request_number, size, (int)size, message);
tf_printf(CYAN "%s RPC RECV[%s]" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B] %.*s\n", connection->name, request_name,
(flags & k_ssb_rpc_flag_end_error) ? "true" : "false", (flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type],
request_number, size, (int)size, message);
}
JSContext* context = connection->ssb->context;
JSValue val = JS_ParseJSON(context, (const char*)message, size, NULL);
if (!JS_IsUndefined(val))
{
tf_ssb_rpc_callback_t* callback = NULL;
void* user_data = NULL;
if (_tf_ssb_connection_get_request_callback(connection, -request_number, &callback, &user_data))
if (have_request)
{
if (callback)
{
@ -1614,35 +1654,14 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
else if (JS_IsObject(val))
{
bool found = false;
char namebuf[256] = "";
JSValue name = JS_GetPropertyStr(context, val, "name");
if (JS_IsArray(context, name))
{
int length = tf_util_get_length(context, name);
int offset = 0;
for (int i = 0; i < length; i++)
{
JSValue part = JS_GetPropertyUint32(context, name, i);
const char* part_str = JS_ToCString(context, part);
offset += snprintf(namebuf + offset, sizeof(namebuf) - offset, "%s%s", i == 0 ? "" : ".", part_str);
JS_FreeCString(context, part_str);
JS_FreeValue(context, part);
}
}
else if (JS_IsString(name))
{
const char* part_str = JS_ToCString(context, name);
snprintf(namebuf, sizeof(namebuf), "%s", part_str);
JS_FreeCString(context, part_str);
}
JS_FreeValue(context, name);
char name[256] = "";
_tf_ssb_name_to_string(context, val, name, sizeof(name));
for (tf_ssb_rpc_callback_node_t* it = connection->ssb->rpc; it; it = it->next)
{
if (_tf_ssb_name_equals(context, val, it->name))
if (strcmp(name, it->name) == 0)
{
tf_ssb_connection_add_request(connection, -request_number, namebuf, NULL, NULL, NULL, NULL);
tf_trace_begin(connection->ssb->trace, it->flattened_name);
tf_ssb_connection_add_request(connection, -request_number, name, NULL, NULL, NULL, NULL);
tf_trace_begin(connection->ssb->trace, it->name);
PRE_CALLBACK(connection->ssb, it->callback);
it->callback(connection, flags, request_number, val, message, size, it->user_data);
POST_CALLBACK(connection->ssb, it->callback);
@ -1651,12 +1670,10 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
break;
}
}
if (!found && !_tf_ssb_name_equals(context, val, (const char*[]) { "Error", NULL }))
if (!found && strcmp(name, "Error") != 0)
{
tf_ssb_connection_add_request(connection, -request_number, namebuf, NULL, NULL, NULL, NULL);
char buffer[256];
_tf_ssb_name_to_string(context, val, buffer, sizeof(buffer));
tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, buffer);
tf_ssb_connection_add_request(connection, -request_number, name, NULL, NULL, NULL, NULL);
tf_ssb_connection_rpc_send_error_method_not_allowed(connection, flags, -request_number, name);
}
}
}
@ -1670,14 +1687,17 @@ static void _tf_ssb_connection_rpc_recv(tf_ssb_connection_t* connection, uint8_t
}
else if ((flags & k_ssb_rpc_mask_type) == k_ssb_rpc_flag_binary)
{
if (connection->ssb->verbose)
{
tf_printf(CYAN "%s RPC RECV" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B]\n", connection->name, (flags & k_ssb_rpc_flag_end_error) ? "true" : "false",
(flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type], request_number, size);
}
tf_ssb_rpc_callback_t* callback = NULL;
void* user_data = NULL;
if (_tf_ssb_connection_get_request_callback(connection, -request_number, &callback, &user_data))
const char* request_name = "<unknown>";
bool have_request = _tf_ssb_connection_get_request_callback(connection, -request_number, &callback, &user_data, &request_name);
if (connection->ssb->verbose)
{
tf_printf(CYAN "%s RPC RECV[%s]" RESET " end/error=%s stream=%s type=%s RN=%d: [%zd B]\n", connection->name, request_name,
(flags & k_ssb_rpc_flag_end_error) ? "true" : "false", (flags & k_ssb_rpc_flag_stream) ? "true" : "false", k_ssb_type_names[flags & k_ssb_rpc_mask_type],
request_number, size);
}
if (have_request)
{
if (callback)
{
@ -1931,8 +1951,13 @@ static void _tf_ssb_connection_destroy(tf_ssb_connection_t* connection, const ch
{
uv_close((uv_handle_t*)&connection->tcp, _tf_ssb_connection_on_close);
}
if (connection->handshake_timer.data && !uv_is_closing((uv_handle_t*)&connection->handshake_timer))
{
uv_close((uv_handle_t*)&connection->handshake_timer, _tf_ssb_connection_on_close);
}
if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->tcp.data && !connection->connect.data && connection->ref_count == 0)
if (JS_IsUndefined(connection->object) && !connection->async.data && !connection->tcp.data && !connection->connect.data && !connection->handshake_timer.data &&
connection->ref_count == 0)
{
tf_free(connection->message_requests);
connection->message_requests = NULL;
@ -1956,7 +1981,7 @@ static void _tf_ssb_connection_on_close(uv_handle_t* handle)
{
tf_ssb_connection_t* connection = handle->data;
handle->data = NULL;
if (connection)
if (connection && connection->closing)
{
_tf_ssb_connection_destroy(connection, "handle closed");
}
@ -2257,6 +2282,11 @@ tf_ssb_t* tf_ssb_create(uv_loop_t* loop, JSContext* context, const char* db_path
uv_timer_start(&ssb->trace_timer, _tf_ssb_trace_timer, 100, 100);
uv_unref((uv_handle_t*)&ssb->trace_timer);
ssb->request_activity_timer.data = ssb;
uv_timer_init(ssb->loop, &ssb->request_activity_timer);
uv_timer_start(&ssb->request_activity_timer, _tf_ssb_request_activity_timer, k_rpc_active_ms, 0);
uv_unref((uv_handle_t*)&ssb->request_activity_timer);
if (!_tf_ssb_load_keys(ssb))
{
tf_printf("Generating a new keypair.\n");
@ -2420,6 +2450,20 @@ static void _tf_ssb_on_handle_close(uv_handle_t* handle)
static void _tf_ssb_on_timer_close(uv_handle_t* handle)
{
tf_ssb_timer_t* timer = handle->data;
for (int i = 0; i < timer->ssb->timers_count; i++)
{
if (timer->ssb->timers[i] == timer)
{
timer->ssb->timers[i] = timer->ssb->timers[--timer->ssb->timers_count];
break;
}
}
if (timer->ssb->shutting_down && !timer->ssb->timers_count)
{
tf_free(timer->ssb->timers);
timer->ssb->timers = NULL;
}
tf_free(handle->data);
}
@ -2459,6 +2503,11 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
uv_close((uv_handle_t*)&ssb->trace_timer, _tf_ssb_on_handle_close);
}
if (ssb->request_activity_timer.data && !uv_is_closing((uv_handle_t*)&ssb->request_activity_timer))
{
uv_close((uv_handle_t*)&ssb->request_activity_timer, _tf_ssb_on_handle_close);
}
if (ssb->server.data && !uv_is_closing((uv_handle_t*)&ssb->server))
{
uv_close((uv_handle_t*)&ssb->server, _tf_ssb_on_handle_close);
@ -2468,14 +2517,11 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
{
uv_close((uv_handle_t*)&ssb->timers[i]->timer, _tf_ssb_on_timer_close);
}
ssb->timers_count = 0;
tf_free(ssb->timers);
ssb->timers = NULL;
tf_printf("Waiting for closes.\n");
while (ssb->broadcast_listener.data || ssb->broadcast_sender.data || ssb->broadcast_timer.data || ssb->broadcast_cleanup_timer.data || ssb->trace_timer.data ||
ssb->server.data || ssb->ref_count)
ssb->server.data || ssb->ref_count || ssb->request_activity_timer.data || ssb->timers_count)
{
uv_run(ssb->loop, UV_RUN_ONCE);
}
@ -2643,6 +2689,15 @@ static void _tf_ssb_connection_process_message_async(uv_async_t* async)
}
}
static void _tf_ssb_connection_handshake_timer_callback(uv_timer_t* timer)
{
tf_ssb_connection_t* connection = timer->data;
if (connection && connection->state != k_tf_ssb_state_verified && connection->state != k_tf_ssb_state_server_verified)
{
_tf_ssb_connection_destroy(connection, "handshake timeout");
}
}
tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, const struct sockaddr_in* addr, const uint8_t* public_key)
{
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
@ -2677,6 +2732,10 @@ tf_ssb_connection_t* tf_ssb_connection_create(tf_ssb_t* ssb, const char* host, c
connection->async.data = connection;
uv_async_init(ssb->loop, &connection->async, _tf_ssb_connection_process_message_async);
connection->handshake_timer.data = connection;
uv_timer_init(ssb->loop, &connection->handshake_timer);
uv_timer_start(&connection->handshake_timer, _tf_ssb_connection_handshake_timer_callback, k_handshake_timeout_ms, 0);
connection->object = JS_NewObjectClass(ssb->context, _connection_class_id);
JS_SetOpaque(connection->object, connection);
char public_key_str[k_id_base64_len] = { 0 };
@ -2712,6 +2771,7 @@ static void _tf_ssb_connection_tunnel_callback(
tf_ssb_connection_t* tunnel = user_data;
if (flags & k_ssb_rpc_flag_end_error)
{
tf_ssb_connection_remove_request(connection, -request_number);
tf_ssb_connection_rpc_send(connection, flags, -request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
tf_ssb_connection_close(tunnel);
}
@ -2721,7 +2781,7 @@ static void _tf_ssb_connection_tunnel_callback(
}
}
tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id)
tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id, int connect_flags)
{
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, portal_id);
@ -2731,12 +2791,17 @@ tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char*
memset(tunnel, 0, sizeof(*tunnel));
snprintf(tunnel->name, sizeof(tunnel->name), "tun%d", s_tunnel_index++);
tunnel->ssb = ssb;
tunnel->flags = connect_flags;
tunnel->tunnel_connection = connection;
tunnel->tunnel_request_number = -request_number;
tunnel->send_request_number = 1;
tunnel->async.data = tunnel;
uv_async_init(ssb->loop, &tunnel->async, _tf_ssb_connection_process_message_async);
tunnel->handshake_timer.data = tunnel;
uv_timer_init(ssb->loop, &tunnel->handshake_timer);
uv_timer_start(&tunnel->handshake_timer, _tf_ssb_connection_handshake_timer_callback, k_handshake_timeout_ms, 0);
tunnel->object = JS_NewObjectClass(ssb->context, _connection_class_id);
JS_SetOpaque(tunnel->object, tunnel);
JS_SetPropertyStr(context, tunnel->object, "id", JS_NewString(context, target_id));
@ -2769,6 +2834,7 @@ typedef struct _connect_t
uv_getaddrinfo_t req;
char host[256];
int port;
int flags;
uint8_t key[k_id_bin_len];
} connect_t;
@ -2781,7 +2847,11 @@ static void _tf_on_connect_getaddrinfo(uv_getaddrinfo_t* addrinfo, int result, s
{
struct sockaddr_in addr = *(struct sockaddr_in*)info->ai_addr;
addr.sin_port = htons(connect->port);
tf_ssb_connection_create(connect->ssb, connect->host, &addr, connect->key);
tf_ssb_connection_t* connection = tf_ssb_connection_create(connect->ssb, connect->host, &addr, connect->key);
if (connection)
{
connection->flags = connect->flags;
}
}
else
{
@ -2793,7 +2863,7 @@ static void _tf_on_connect_getaddrinfo(uv_getaddrinfo_t* addrinfo, int result, s
tf_free(connect);
}
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key)
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags)
{
if (ssb->shutting_down)
{
@ -2803,6 +2873,7 @@ void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* ke
*connect = (connect_t) {
.ssb = ssb,
.port = port,
.flags = connect_flags,
.req.data = connect,
};
char id[k_id_base64_len] = { 0 };
@ -2814,7 +2885,7 @@ void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* ke
int r = uv_getaddrinfo(ssb->loop, &connect->req, _tf_on_connect_getaddrinfo, host, NULL, &(struct addrinfo) { .ai_family = AF_INET });
if (r < 0)
{
tf_printf("uv_getaddrinfo: %s\n", uv_strerror(r));
tf_printf("uv_getaddrinfo(%s): %s\n", host, uv_strerror(r));
tf_free(connect);
tf_ssb_unref(ssb);
}
@ -2859,6 +2930,10 @@ static void _tf_ssb_on_connection(uv_stream_t* stream, int status)
return;
}
connection->handshake_timer.data = connection;
uv_timer_init(ssb->loop, &connection->handshake_timer);
uv_timer_start(&connection->handshake_timer, _tf_ssb_connection_handshake_timer_callback, k_handshake_timeout_ms, 0);
struct sockaddr_storage addr = { 0 };
int size = sizeof(addr);
if (uv_tcp_getpeername(&connection->tcp, (struct sockaddr*)&addr, &size) == 0)
@ -2870,10 +2945,10 @@ static void _tf_ssb_on_connection(uv_stream_t* stream, int status)
connection->next = ssb->connections;
ssb->connections = connection;
ssb->connections_count++;
_tf_ssb_notify_connections_changed(ssb, k_tf_ssb_change_create, connection);
connection->state = k_tf_ssb_state_server_wait_hello;
_tf_ssb_connection_read_start(connection);
_tf_ssb_notify_connections_changed(ssb, k_tf_ssb_change_create, connection);
}
static void _tf_ssb_send_broadcast(tf_ssb_t* ssb, struct sockaddr_in* address, struct sockaddr_in* netmask)
@ -3089,12 +3164,12 @@ static bool _tf_ssb_parse_broadcast(const char* in_broadcast, tf_ssb_broadcast_t
return false;
}
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address)
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags)
{
tf_ssb_broadcast_t broadcast = { 0 };
if (_tf_ssb_parse_broadcast(address, &broadcast))
{
tf_ssb_connect(ssb, broadcast.host, ntohs(broadcast.addr.sin_port), broadcast.pub);
tf_ssb_connect(ssb, broadcast.host, ntohs(broadcast.addr.sin_port), broadcast.pub, connect_flags);
}
else
{
@ -3316,11 +3391,6 @@ tf_ssb_connection_t* tf_ssb_connection_get(tf_ssb_t* ssb, const char* id)
tf_ssb_id_str_to_bin(pub, id);
for (tf_ssb_connection_t* connection = ssb->connections; connection; connection = connection->next)
{
if (connection->tunnel_connection)
{
continue;
}
if (memcmp(connection->serverpub, pub, k_id_bin_len) == 0)
{
return connection;
@ -3443,47 +3513,18 @@ void tf_ssb_remove_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_connection
}
}
void tf_ssb_add_rpc_callback(tf_ssb_t* ssb, const char** name, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data)
void tf_ssb_add_rpc_callback(tf_ssb_t* ssb, const char* name, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data)
{
size_t name_len = 0;
int name_count = 0;
for (int i = 0; name[i]; i++)
{
name_count++;
name_len += strlen(name[i]) + 1;
}
tf_ssb_rpc_callback_node_t* node = tf_malloc(sizeof(tf_ssb_rpc_callback_node_t) + (name_count + 1) * sizeof(const char*) + name_len + name_len + 3);
size_t name_len = strlen(name);
tf_ssb_rpc_callback_node_t* node = tf_malloc(sizeof(tf_ssb_rpc_callback_node_t) + name_len + 1);
*node = (tf_ssb_rpc_callback_node_t) {
.name = (const char**)(node + 1),
.flattened_name = (const char*)(node + 1) + (name_count + 1) * sizeof(const char*) + name_len,
.name = (const char*)(node + 1),
.callback = callback,
.cleanup = cleanup,
.user_data = user_data,
.next = ssb->rpc,
};
char* p = (char*)(node + 1) + (name_count + 1) * sizeof(const char*);
for (int i = 0; i < name_count; i++)
{
size_t len = strlen(name[i]);
memcpy(p, name[i], len + 1);
node->name[i] = p;
p += len + 1;
}
char* flattened_name = (char*)node->flattened_name;
for (int i = 0; i < name_count; i++)
{
size_t length = strlen(name[i]);
memcpy(flattened_name, name[i], length);
flattened_name += length;
if (i != name_count - 1)
{
*flattened_name++ = '.';
}
}
*flattened_name++ = '(';
*flattened_name++ = ')';
*flattened_name++ = '\0';
node->name[name_count] = NULL;
memcpy((char*)node->name, name, name_len + 1);
ssb->rpc = node;
ssb->rpc_count++;
}
@ -4167,19 +4208,16 @@ static void _tf_ssb_scheduled_timer(uv_timer_t* handle)
{
tf_ssb_timer_t* timer = handle->data;
timer->callback(timer->ssb, timer->user_data);
for (int i = 0; i < timer->ssb->timers_count; i++)
{
if (timer->ssb->timers[i] == timer)
{
timer->ssb->timers[i] = timer->ssb->timers[--timer->ssb->timers_count];
break;
}
}
uv_close((uv_handle_t*)handle, _tf_ssb_on_timer_close);
}
void tf_ssb_schedule_work(tf_ssb_t* ssb, int delay_ms, void (*callback)(tf_ssb_t* ssb, void* user_data), void* user_data)
{
if (ssb->shutting_down)
{
return;
}
ssb->timers = tf_resize_vec(ssb->timers, sizeof(uv_timer_t*) * (ssb->timers_count + 1));
tf_ssb_timer_t* timer = tf_malloc(sizeof(tf_ssb_timer_t));
*timer = (tf_ssb_timer_t)
@ -4229,11 +4267,13 @@ JSValue tf_ssb_connection_requests_to_object(tf_ssb_connection_t* connection)
{
JSContext* context = connection->ssb->context;
JSValue object = JS_NewArray(context);
uint64_t now_ms = uv_now(connection->ssb->loop);
for (int i = 0; i < connection->requests_count; i++)
{
JSValue request = JS_NewObject(context);
JS_SetPropertyStr(context, request, "name", JS_NewString(context, connection->requests[i].name));
JS_SetPropertyStr(context, request, "request_number", JS_NewInt32(context, connection->requests[i].request_number));
JS_SetPropertyStr(context, request, "active", JS_NewBool(context, (now_ms - connection->requests[i].last_active) < k_rpc_active_ms));
JS_SetPropertyUint32(context, object, i, request);
}
return object;
@ -4267,3 +4307,41 @@ void tf_ssb_connection_adjust_write_count(tf_ssb_connection_t* connection, int d
connection->active_write_count += delta;
_tf_ssb_connection_dispatch_scheduled(connection);
}
void tf_ssb_sync_start(tf_ssb_t* ssb)
{
tf_ssb_connections_sync_start(ssb->connections_tracker);
}
bool tf_ssb_tunnel_create(tf_ssb_t* ssb, const char* portal_id, const char* target_id, int connect_flags)
{
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, portal_id);
if (connection && !tf_ssb_connection_get(ssb, target_id))
{
JSContext* context = ssb->context;
int32_t request_number = tf_ssb_connection_next_request_number(connection);
JSValue message = JS_NewObject(context);
JSValue name = JS_NewArray(context);
JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel"));
JS_SetPropertyUint32(context, name, 1, JS_NewString(context, "connect"));
JS_SetPropertyStr(context, message, "name", name);
JSValue arg = JS_NewObject(context);
JS_SetPropertyStr(context, arg, "portal", JS_NewString(context, portal_id));
JS_SetPropertyStr(context, arg, "target", JS_NewString(context, target_id));
JSValue args = JS_NewArray(context);
JS_SetPropertyUint32(context, args, 0, arg);
JS_SetPropertyStr(context, message, "args", args);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "duplex"));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_new_request, request_number, "tunnel.connect", message, NULL, NULL, NULL);
JS_FreeValue(context, message);
tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id, connect_flags);
return true;
}
return false;
}
int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection)
{
return connection->flags;
}

@ -4,8 +4,8 @@
#include "mem.h"
#include "ssb.h"
#include "uv.h"
#include "sqlite3.h"
#include "uv.h"
#include <string.h>
@ -21,6 +21,11 @@ typedef struct _tf_ssb_connections_t
static void _tf_ssb_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_change_t change, tf_ssb_connection_t* connection, void* user_data)
{
if (!connection || tf_ssb_is_shutting_down(ssb))
{
return;
}
tf_ssb_connections_t* connections = user_data;
switch (change)
{
@ -101,7 +106,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];
if (tf_ssb_id_str_to_bin(key_bin, next->key))
{
tf_ssb_connect(ssb, next->host, next->port, key_bin);
tf_ssb_connect(ssb, next->host, next->port, key_bin, 0);
}
}
tf_free(next);
@ -262,3 +267,82 @@ void tf_ssb_connections_set_succeeded(tf_ssb_connections_t* connections, const c
snprintf(update->key, sizeof(update->key), "%s", key);
_tf_ssb_connections_queue_update(connections, update);
}
static void _tf_ssb_connections_sync_broadcast_visit(
const char* host, const struct sockaddr_in* addr, tf_ssb_broadcast_origin_t origin, tf_ssb_connection_t* tunnel, const uint8_t* pub, void* user_data)
{
tf_ssb_t* ssb = user_data;
if (tunnel)
{
char target_id[k_id_base64_len] = { 0 };
if (tf_ssb_id_bin_to_str(target_id, sizeof(target_id), pub))
{
char portal_id[k_id_base64_len] = { 0 };
if (tf_ssb_connection_get_id(tunnel, portal_id, sizeof(portal_id)))
{
tf_ssb_tunnel_create(ssb, portal_id, target_id, k_tf_ssb_connect_flag_one_shot);
}
}
}
else
{
tf_ssb_connect(ssb, host, ntohs(addr->sin_port), pub, k_tf_ssb_connect_flag_one_shot);
}
}
typedef struct _tf_ssb_connections_get_all_work_t
{
char** connections;
int connections_count;
} tf_ssb_connections_get_all_work_t;
static void _tf_ssb_connections_get_all_work(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_connections_get_all_work_t* work = user_data;
sqlite3_stmt* statement;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
if (sqlite3_prepare(db, "SELECT host, port, key FROM connections ORDER BY last_attempt", -1, &statement, NULL) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
const char* host = (const char*)sqlite3_column_text(statement, 0);
int port = sqlite3_column_int(statement, 1);
const char* key = (const char*)sqlite3_column_text(statement, 2);
char connection[1024] = { 0 };
snprintf(connection, sizeof(connection), "net:%s:%d~shs:%s", host, port, *key == '@' ? key + 1 : key);
char* dot = strrchr(connection, '.');
if (dot && strcmp(dot, ".ed25519") == 0)
{
*dot = '\0';
}
work->connections = tf_resize_vec(work->connections, sizeof(char*) * (work->connections_count + 1));
work->connections[work->connections_count++] = tf_strdup(connection);
}
sqlite3_finalize(statement);
}
else
{
tf_printf("prepare: %s\n", sqlite3_errmsg(db));
}
tf_ssb_release_db_reader(ssb, db);
}
static void _tf_ssb_connections_get_all_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
tf_ssb_connections_get_all_work_t* work = user_data;
for (int i = 0; i < work->connections_count; i++)
{
tf_ssb_connect_str(ssb, work->connections[i], k_tf_ssb_connect_flag_one_shot);
tf_free(work->connections[i]);
}
tf_free(work->connections);
tf_free(work);
}
void tf_ssb_connections_sync_start(tf_ssb_connections_t* connections)
{
tf_ssb_connections_get_all_work_t* work = tf_malloc(sizeof(tf_ssb_connections_get_all_work_t));
*work = (tf_ssb_connections_get_all_work_t) { 0 };
tf_ssb_run_work(connections->ssb, _tf_ssb_connections_get_all_work, _tf_ssb_connections_get_all_after_work, work);
tf_ssb_visit_broadcasts(connections->ssb, _tf_ssb_connections_sync_broadcast_visit, connections->ssb);
}

@ -53,4 +53,10 @@ void tf_ssb_connections_set_attempted(tf_ssb_connections_t* connections, const c
*/
void tf_ssb_connections_set_succeeded(tf_ssb_connections_t* connections, const char* host, int port, const char* key);
/**
** Initiate an immediate sync.
** @param connections The connections tracker.
*/
void tf_ssb_connections_sync_start(tf_ssb_connections_t* connections);
/** @} */

@ -133,6 +133,7 @@ void tf_ssb_db_init(tf_ssb_t* ssb)
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_sequence_index ON messages (author, sequence)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_author_timestamp_index ON messages (author, timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages (timestamp)");
_tf_ssb_db_exec(db, "CREATE INDEX IF NOT EXISTS messages_type_timestamp_index ON messages (content ->> 'type', timestamp)");
_tf_ssb_db_exec(db,
"CREATE TABLE IF NOT EXISTS blobs ("
" id TEXT PRIMARY KEY,"
@ -1786,6 +1787,65 @@ bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, cons
return result;
}
bool tf_ssb_db_remove_property(tf_ssb_t* ssb, const char* id, const char* key)
{
bool result = false;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
return result;
}
bool tf_ssb_db_remove_value_from_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
bool result = false;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db,
"UPDATE properties SET value = json_remove(properties.value, entry.fullkey) FROM json_each(properties.value) AS entry WHERE properties.id = ? AND properties.key = ? "
"AND entry.value = ?",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
return result;
}
bool tf_ssb_db_add_value_to_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value)
{
bool result = false;
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db,
"INSERT INTO properties (id, key, value) VALUES (?1, ?2, json_array(?3)) ON CONFLICT DO UPDATE SET value = json_insert(properties.value, '$[#]', ?3) WHERE "
"properties.id = ?1 AND properties.key = ?2 AND NOT EXISTS (SELECT 1 FROM json_each(properties.value) AS entry WHERE entry.value = ?3)",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, value, -1, NULL) == SQLITE_OK)
{
result = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) != 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_writer(ssb, db);
return result;
}
bool tf_ssb_db_identity_get_active(sqlite3* db, const char* user, const char* package_owner, const char* package_name, char* out_identity, size_t out_identity_size)
{
sqlite3_stmt* statement = NULL;
@ -1945,3 +2005,24 @@ bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id)
}
return verified;
}
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission)
{
bool has_permission = false;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db,
"SELECT COUNT(*) FROM properties, json_each(properties.value -> 'permissions' -> ?) AS permission WHERE properties.id = 'core' AND properties.key = 'settings' AND "
"permission.value = ?",
-1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, permission, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW)
{
has_permission = sqlite3_column_int64(statement, 0) > 0;
}
sqlite3_finalize(statement);
}
tf_ssb_release_db_reader(ssb, db);
return has_permission;
}

@ -399,6 +399,35 @@ const char* tf_ssb_db_get_property(tf_ssb_t* ssb, const char* id, const char* ke
*/
bool tf_ssb_db_set_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
/**
** Remove an entry in the properties table.
** @param ssb The SSB instance.
** @param id The user.
** @param key The property key.
** @return true if the property was removed successfully.
*/
bool tf_ssb_db_remove_property(tf_ssb_t* ssb, const char* id, const char* key);
/**
** Remove a value from an entry in the properties table that is a JSON array.
** @param ssb The SSB instance.
** @param id The user.
** @param key The property key.
** @param value The value to remove from the JSON array.
** @return true if the property was updated.
*/
bool tf_ssb_db_remove_value_from_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
/**
** Ensure a value is in an entry in the properties table that is a JSON array.
** @param ssb The SSB instance.
** @param id The user.
** @param key The property key.
** @param value The value to add to the JSON array.
** @return true if the property was updated.
*/
bool tf_ssb_db_add_value_to_array_property(tf_ssb_t* ssb, const char* id, const char* key, const char* value);
/**
** Resolve a hostname to its index path by global settings.
** @param ssb The SSB instance.
@ -416,6 +445,15 @@ void tf_ssb_db_resolve_index_async(tf_ssb_t* ssb, const char* host, void (*callb
*/
bool tf_ssb_db_verify(tf_ssb_t* ssb, const char* id);
/**
** Check if a user has a specific permission.
** @param ssb The SSB instance.
** @param id The user ID.
** @param permission The name of the permission.
** @return true If the user has the requested permission.
*/
bool tf_ssb_db_user_has_permission(tf_ssb_t* ssb, const char* id, const char* permission);
/**
** An SQLite authorizer callback. See https://www.sqlite.org/c3ref/set_authorizer.html for use.
** @param user_data User data registered with the authorizer.

@ -69,7 +69,7 @@ static void _tf_ssb_export_scandir(uv_fs_t* req)
int r = uv_fs_unlink(tf_ssb_get_loop(export->ssb), &req, path, NULL);
if (r)
{
tf_printf("Failed to unlink %s: %s.", path, uv_strerror(r));
tf_printf("Failed to unlink %s: %s.\n", path, uv_strerror(r));
}
uv_fs_req_cleanup(&req);
tf_free(path);
@ -199,7 +199,7 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
int r = uv_fs_scandir(tf_ssb_get_loop(ssb), &export.req, file_path, 0, _tf_ssb_export_scandir);
if (r)
{
tf_printf("Failed to scan directory %s: %s.", file_path, uv_strerror(r));
tf_printf("Failed to scan directory %s: %s.\n", file_path, uv_strerror(r));
}
while (!export.done)
{

@ -65,6 +65,14 @@ typedef enum _tf_ssb_message_flags_t
k_tf_ssb_message_flag_sequence_before_author = 1,
} tf_ssb_message_flags_t;
/**
** Flags affecting an SSB connection.
*/
typedef enum _tf_ssb_connect_flags_t
{
k_tf_ssb_connect_flag_one_shot = 0x1,
} tf_ssb_connect_flags_t;
/** An SSB instance. */
typedef struct _tf_ssb_t tf_ssb_t;
/** An SSB connection. */
@ -346,15 +354,17 @@ int tf_ssb_get_connections(tf_ssb_t* ssb, tf_ssb_connection_t** out_connections,
** @param host The host name or address.
** @param port The host's SHS port.
** @param key The host's SSB identity.
** @param connect_flags Flags affecting the connection.
*/
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key);
void tf_ssb_connect(tf_ssb_t* ssb, const char* host, int port, const uint8_t* key, int connect_flags);
/**
** Establish an SHS connection with a host by string address.
** @param ssb The SSB instance.
** @param address The address.
** @param connect_flags Flags affecting the connection.
*/
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address);
void tf_ssb_connect_str(tf_ssb_t* ssb, const char* address, int connect_flags);
/**
** Begin listening for SHS connections on the given port.
@ -380,8 +390,9 @@ void tf_ssb_server_close(tf_ssb_t* ssb);
/**
** Close all active SHS connections.
** @param ssb The SSB instance.
** @param reason Reason for the close.
*/
void tf_ssb_close_all(tf_ssb_t* ssb);
void tf_ssb_close_all(tf_ssb_t* ssb, const char* reason);
/**
** Send a graceful close message to all active SHS connections.
@ -676,12 +687,12 @@ typedef void(tf_ssb_rpc_callback_t)(tf_ssb_connection_t* connection, uint8_t fla
/**
** Register a MUXRPC callback by name.
** @param ssb The SSB instance.
** @param name The NULL-terminated name.
** @param name The RPC name as a .-separated string.
** @param callback The callback.
** @param cleanup A function to be called when the callback is removed.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_add_rpc_callback(tf_ssb_t* ssb, const char** name, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
void tf_ssb_add_rpc_callback(tf_ssb_t* ssb, const char* name, tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
** Remove a MUXRPC callback.
@ -703,8 +714,9 @@ void tf_ssb_remove_rpc_callback(tf_ssb_t* ssb, const char** name, tf_ssb_rpc_cal
** @param callback A callback to call if a response is received.
** @param cleanup A callback to call if the callback is removed.
** @param user_data User data to pass to the callback.
** @return true If the message was queued to send, false if the connection or request were invalid.
*/
void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, const uint8_t* message, size_t size,
bool tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, const uint8_t* message, size_t size,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
@ -717,8 +729,9 @@ void tf_ssb_connection_rpc_send(tf_ssb_connection_t* connection, uint8_t flags,
** @param callback A callback to call if a response is received.
** @param cleanup A callback to call if the callback is removed.
** @param user_data User data to pass to the callback.
** @return true If the message was queued to send, false if the connection or request were invalid.
*/
void tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, JSValue message,
bool tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* new_request_name, JSValue message,
tf_ssb_rpc_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
@ -727,8 +740,9 @@ void tf_ssb_connection_rpc_send_json(tf_ssb_connection_t* connection, uint8_t fl
** @param flags The message flags.
** @param request_number The request number.
** @param error The error string.
** @return true If the message was queued to send, false if the connection or request were invalid.
*/
void tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* error);
bool tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* error);
/**
** Send a MUXRPC "method not allowed" error message.
@ -736,8 +750,9 @@ void tf_ssb_connection_rpc_send_error(tf_ssb_connection_t* connection, uint8_t f
** @param flags The message flags.
** @param request_number The request number.
** @param name The name of the not-allowed method.
** @return true If the message was queued to send, false if the connection or request were invalid.
*/
void tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* name);
bool tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* connection, uint8_t flags, int32_t request_number, const char* name);
/**
** Register a callback to be called when a message is received for the given
@ -867,9 +882,10 @@ void tf_ssb_connection_remove_room_attendant(tf_ssb_connection_t* connection, co
** @param portal_id The identity of the tunnel intermediary.
** @param request_number The tunnel request.
** @param target_id The identity being tunneled to.
** @param connect_flags Flags affecting the connection.
** @return The new tunnel connection.
*/
tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id);
tf_ssb_connection_t* tf_ssb_connection_tunnel_create(tf_ssb_t* ssb, const char* portal_id, int32_t request_number, const char* target_id, int connect_flags);
/**
** Get the request number on which to send EBT responses.
@ -1073,4 +1089,26 @@ 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);
/**
** Initiate a tunnel connection.
** @param ssb The SSB instance.
** @param portal_id The public key of the instance through which to tunnel.
** @param target_id The public key of the instance with which to establish a connection.
** @param connect_flags Flags affecting the connection.
** @return true if the tunnel instance was found.
*/
bool tf_ssb_tunnel_create(tf_ssb_t* ssb, const char* portal_id, const char* target_id, int connect_flags);
/**
** Initiate a one time sync operation.
** @param ssb The SSB instance.
*/
void tf_ssb_sync_start(tf_ssb_t* ssb);
/**
** Get a connection's flags.
** @param connection The connection.
*/
int tf_ssb_connection_get_flags(tf_ssb_connection_t* connection);
/** @} */

@ -155,7 +155,7 @@ static void _tf_ssb_import_recursive_add_files(tf_ssb_t* ssb, uv_loop_t* loop, J
}
else
{
tf_printf("Failed to scan directory %s: %s.", path, uv_strerror(r));
tf_printf("Failed to scan directory %s: %s.\n", path, uv_strerror(r));
}
uv_fs_req_cleanup(&req);
}
@ -260,7 +260,7 @@ void tf_ssb_import(tf_ssb_t* ssb, const char* user, const char* path)
}
else
{
tf_printf("Failed to scan directory %s: %s.", path, uv_strerror(r));
tf_printf("Failed to scan directory %s: %s.\n", path, uv_strerror(r));
}
uv_fs_req_cleanup(&req);
}

@ -13,8 +13,8 @@
#include "sodium/crypto_secretbox.h"
#include "sodium/crypto_sign.h"
#include "sodium/randombytes.h"
#include "string.h"
#include "sqlite3.h"
#include "string.h"
#include "uv.h"
#include <assert.h>
@ -360,6 +360,109 @@ static JSValue _tf_ssb_set_server_following_me(JSContext* context, JSValueConst
return result;
}
typedef struct _swap_with_server_identity_t
{
char server_id[k_id_base64_len];
char id[k_id_base64_len];
JSValue promise[2];
char* error;
char user[];
} swap_with_server_identity_t;
static void _tf_ssb_swap_with_server_identity_work(tf_ssb_t* ssb, void* user_data)
{
swap_with_server_identity_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, work->user, "administration"))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
char* error = NULL;
if (sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &error) == SQLITE_OK)
{
sqlite3_stmt* statement = NULL;
if (sqlite3_prepare(db, "UPDATE identities SET user = ? WHERE user = ? AND '@' || public_key = ?", -1, &statement, NULL) == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 3, work->server_id, -1, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1 &&
sqlite3_reset(statement) == SQLITE_OK && sqlite3_bind_text(statement, 1, ":admin", -1, NULL) == SQLITE_OK &&
sqlite3_bind_text(statement, 2, work->user, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->id, -1, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(db) == 1)
{
error = NULL;
if (sqlite3_exec(db, "COMMIT TRANSACTION", NULL, NULL, &error) != SQLITE_OK)
{
work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db) ? sqlite3_errmsg(db) : "swap failed");
}
sqlite3_finalize(statement);
}
else
{
work->error = tf_strdup(sqlite3_errmsg(db));
}
}
else
{
work->error = error ? tf_strdup(error) : tf_strdup(sqlite3_errmsg(db));
}
tf_ssb_release_db_writer(ssb, db);
}
else
{
work->error = tf_strdup("not administrator");
}
}
static void _tf_ssb_swap_with_server_identity_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
swap_with_server_identity_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue error = JS_UNDEFINED;
if (work->error)
{
JSValue arg = JS_ThrowInternalError(context, "%s", work->error);
JSValue exception = JS_GetException(context);
error = JS_Call(context, work->promise[1], JS_UNDEFINED, 1, &exception);
tf_free(work->error);
JS_FreeValue(context, exception);
JS_FreeValue(context, arg);
}
else
{
error = JS_Call(context, work->promise[0], JS_UNDEFINED, 0, NULL);
}
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_swap_with_server_identity(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)
{
size_t user_length = 0;
const char* user = JS_ToCStringLen(context, &user_length, argv[0]);
const char* id = JS_ToCString(context, argv[1]);
swap_with_server_identity_t* work = tf_malloc(sizeof(swap_with_server_identity_t) + user_length + 1);
*work = (swap_with_server_identity_t) { 0 };
tf_ssb_whoami(ssb, work->server_id, sizeof(work->server_id));
snprintf(work->id, sizeof(work->id), "%s", id);
memcpy(work->user, user, user_length + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_ssb_run_work(ssb, _tf_ssb_swap_with_server_identity_work, _tf_ssb_swap_with_server_identity_after_work, work);
JS_FreeCString(context, user);
JS_FreeCString(context, id);
}
return result;
}
typedef struct _identities_visit_t
{
JSContext* context;
@ -381,6 +484,14 @@ static void _tf_ssb_getIdentities_visit(const char* identity, void* user_data)
static void _tf_ssb_get_identities_work(tf_ssb_t* ssb, void* user_data)
{
identities_visit_t* work = user_data;
if (tf_ssb_db_user_has_permission(ssb, work->user, "administration"))
{
char id[k_id_base64_len] = "";
if (tf_ssb_whoami(ssb, id, sizeof(id)))
{
_tf_ssb_getIdentities_visit(*id == '@' ? id + 1 : id, work);
}
}
tf_ssb_db_identity_visit(ssb, work->user, _tf_ssb_getIdentities_visit, user_data);
}
@ -445,6 +556,10 @@ static void _tf_ssb_get_private_key_work(tf_ssb_t* ssb, void* user_data)
{
get_private_key_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
}
static void _tf_ssb_get_private_key_after_work(tf_ssb_t* ssb, int status, void* user_data)
@ -625,6 +740,14 @@ static void _tf_ssb_getIdentityInfo_visit(const char* identity, void* data)
static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
{
identity_info_work_t* request = user_data;
char id[k_id_base64_len] = "";
if (tf_ssb_db_user_has_permission(ssb, request->name, "administration"))
{
if (tf_ssb_whoami(ssb, id, sizeof(id)))
{
_tf_ssb_getIdentityInfo_visit(*id == '@' ? id + 1 : id, request);
}
}
tf_ssb_db_identity_visit(ssb, request->name, _tf_ssb_getIdentityInfo_visit, request);
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
@ -637,12 +760,15 @@ static void _tf_ssb_getIdentityInfo_work(tf_ssb_t* ssb, void* user_data)
" messages.content ->> 'name' AS name "
" FROM messages "
" JOIN identities ON messages.author = ('@' || identities.public_key) "
" WHERE identities.user = ? AND json_extract(messages.content, '$.type') = 'about' AND content ->> 'about' = messages.author AND name IS NOT NULL) "
" WHERE "
" (identities.user = ? OR identities.public_key = ?) AND "
" json_extract(messages.content, '$.type') = 'about' AND "
" content ->> 'about' = messages.author AND name IS NOT NULL) "
"WHERE author_rank = 1 ",
-1, &statement, NULL);
if (request->result == SQLITE_OK)
{
if (sqlite3_bind_text(statement, 1, request->name, -1, NULL) == SQLITE_OK)
if (sqlite3_bind_text(statement, 1, request->name, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, *id == '@' ? id + 1 : id, -1, NULL) == SQLITE_OK)
{
int r = SQLITE_OK;
while ((r = sqlite3_step(statement)) == SQLITE_ROW)
@ -777,6 +903,10 @@ static void _tf_ssb_append_message_with_identity_get_key_work(tf_ssb_t* ssb, voi
{
append_message_t* work = user_data;
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, work->user, work->id, work->private_key, sizeof(work->private_key));
if (!work->got_private_key && tf_ssb_db_user_has_permission(ssb, work->user, "administration"))
{
work->got_private_key = tf_ssb_db_identity_get_private_key(ssb, ":admin", work->id, work->private_key, sizeof(work->private_key));
}
tf_ssb_db_get_latest_message_by_author(ssb, work->id, &work->previous_sequence, work->previous_id, sizeof(work->previous_id));
}
@ -985,6 +1115,10 @@ static JSValue _tf_ssb_connections(JSContext* context, JSValueConst this_val, in
JS_SetPropertyStr(context, object, "tunnel", JS_NewInt32(context, tunnel_index));
}
JS_SetPropertyStr(context, object, "requests", tf_ssb_connection_requests_to_object(connection));
JSValue flags_object = JS_NewObject(context);
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, object, "flags", flags_object);
JS_SetPropertyUint32(context, result, i, object);
}
}
@ -1509,7 +1643,7 @@ static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int ar
{
const char* address_str = JS_ToCString(context, args);
tf_printf("Connecting to %s\n", address_str);
tf_ssb_connect_str(ssb, address_str);
tf_ssb_connect_str(ssb, address_str, 0);
JS_FreeCString(context, address_str);
}
else
@ -1526,7 +1660,7 @@ static JSValue _tf_ssb_connect(JSContext* context, JSValueConst this_val, int ar
tf_printf("Connecting to %s:%d\n", address_str, port_int);
uint8_t pubkey_bin[k_id_bin_len];
tf_ssb_id_str_to_bin(pubkey_bin, pubkey_str);
tf_ssb_connect(ssb, address_str, port_int, pubkey_bin);
tf_ssb_connect(ssb, address_str, port_int, pubkey_bin, 0);
}
else
{
@ -1799,37 +1933,15 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
static JSValue _tf_ssb_createTunnel(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
JSValue result = JS_UNDEFINED;
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
const char* portal_id = JS_ToCString(context, argv[0]);
const char* target_id = JS_ToCString(context, argv[1]);
tf_ssb_connection_t* connection = tf_ssb_connection_get(ssb, portal_id);
if (connection)
{
int32_t request_number = tf_ssb_connection_next_request_number(connection);
JSValue message = JS_NewObject(context);
JSValue name = JS_NewArray(context);
JS_SetPropertyUint32(context, name, 0, JS_NewString(context, "tunnel"));
JS_SetPropertyUint32(context, name, 1, JS_NewString(context, "connect"));
JS_SetPropertyStr(context, message, "name", name);
JSValue arg = JS_NewObject(context);
JS_SetPropertyStr(context, arg, "portal", JS_NewString(context, portal_id));
JS_SetPropertyStr(context, arg, "target", JS_NewString(context, target_id));
JSValue args = JS_NewArray(context);
JS_SetPropertyUint32(context, args, 0, arg);
JS_SetPropertyStr(context, message, "args", args);
JS_SetPropertyStr(context, message, "type", JS_NewString(context, "duplex"));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_new_request, request_number, "tunnel.connect", message, NULL, NULL, NULL);
JS_FreeValue(context, message);
tf_ssb_connection_tunnel_create(ssb, portal_id, request_number, target_id);
result = JS_TRUE;
}
bool result = tf_ssb_tunnel_create(ssb, portal_id, target_id, 0);
JS_FreeCString(context, target_id);
JS_FreeCString(context, portal_id);
return result;
return result ? JS_TRUE : JS_FALSE;
}
enum
@ -1839,7 +1951,7 @@ enum
static bool _tf_ssb_get_private_key_curve25519(sqlite3* db, const char* user, const char* identity, uint8_t out_private_key[static crypto_sign_SECRETKEYBYTES])
{
if (!user || !identity || !out_private_key)
if (!user || !identity)
{
tf_printf("user=%p identity=%p out_private_key=%p\n", user, identity, out_private_key);
return false;
@ -2279,6 +2391,117 @@ static JSValue _tf_ssb_following(JSContext* context, JSValueConst this_val, int
return result;
}
static JSValue _tf_ssb_sync(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_ssb_t* ssb = JS_GetOpaque(this_val, _tf_ssb_classId);
tf_ssb_sync_start(ssb);
return JS_UNDEFINED;
}
typedef struct _set_user_permission_t
{
tf_ssb_t* ssb;
JSContext* context;
const char* user;
const char* package_owner;
const char* package_name;
const char* permission;
bool allow;
bool result;
JSValue promise[2];
} set_user_permission_t;
static void _tf_ssb_set_user_permission_work(tf_ssb_t* ssb, void* user_data)
{
set_user_permission_t* work = user_data;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
/* XXX: Do this with one DB writer. */
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
if (settings)
{
JSValue settings_value = JS_ParseJSON(context, settings, strlen(settings), NULL);
JSValue user_permissions = JS_GetPropertyStr(context, settings_value, "userPermissions");
if (JS_IsUndefined(user_permissions))
{
user_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "userPermissions", JS_DupValue(context, user_permissions));
}
JSValue user = JS_GetPropertyStr(context, user_permissions, work->user);
if (JS_IsUndefined(user))
{
user = JS_NewObject(context);
JS_SetPropertyStr(context, user_permissions, work->user, JS_DupValue(context, user));
}
JSValue package_owner = JS_GetPropertyStr(context, user, work->package_owner);
if (JS_IsUndefined(package_owner))
{
package_owner = JS_NewObject(context);
JS_SetPropertyStr(context, user, work->package_owner, JS_DupValue(context, package_owner));
}
JSValue package_name = JS_GetPropertyStr(context, package_owner, work->package_name);
if (JS_IsUndefined(package_name))
{
package_name = JS_NewObject(context);
JS_SetPropertyStr(context, package_owner, work->package_name, package_name);
}
JSValue permission = JS_GetPropertyStr(context, package_name, work->permission);
if (JS_ToBool(context, permission) != work->allow)
{
JS_SetPropertyStr(context, package_name, work->permission, JS_NewBool(context, work->allow));
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
work->result = tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
else
{
work->result = true;
}
}
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _tf_ssb_set_user_permission_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
set_user_permission_t* work = user_data;
JSContext* context = work->context;
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->package_owner);
JS_FreeCString(context, work->package_name);
JS_FreeCString(context, work->permission);
JSValue error = JS_Call(context, work->result ? work->promise[0] : work->promise[0], JS_UNDEFINED, 0, NULL);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
tf_free(work);
}
static JSValue _tf_ssb_set_user_permission(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
set_user_permission_t* set = tf_malloc(sizeof(set_user_permission_t));
*set = (set_user_permission_t) {
.ssb = JS_GetOpaque(this_val, _tf_ssb_classId),
.context = context,
.user = JS_ToCString(context, argv[0]),
.package_owner = JS_ToCString(context, argv[1]),
.package_name = JS_ToCString(context, argv[2]),
.permission = JS_ToCString(context, argv[3]),
.allow = JS_ToBool(context, argv[4]),
};
JSValue result = JS_NewPromiseCapability(context, set->promise);
tf_ssb_run_work(set->ssb, _tf_ssb_set_user_permission_work, _tf_ssb_set_user_permission_after_work, set);
return result;
}
void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
{
JS_NewClassID(&_tf_ssb_classId);
@ -2300,10 +2523,12 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
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, "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, "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, "privateMessageEncrypt", JS_NewCFunction(context, _tf_ssb_private_message_encrypt, "privateMessageEncrypt", 4));
JS_SetPropertyStr(context, object, "privateMessageDecrypt", JS_NewCFunction(context, _tf_ssb_private_message_decrypt, "privateMessageDecrypt", 3));
JS_SetPropertyStr(context, object, "setUserPermission", JS_NewCFunction(context, _tf_ssb_set_user_permission, "setUserPermission", 5));
/* Write. */
JS_SetPropertyStr(context, object, "appendMessageWithIdentity", JS_NewCFunction(context, _tf_ssb_appendMessageWithIdentity, "appendMessageWithIdentity", 3));
@ -2323,6 +2548,7 @@ void tf_ssb_register(JSContext* context, tf_ssb_t* ssb)
JS_SetPropertyStr(context, object, "connect", JS_NewCFunction(context, _tf_ssb_connect, "connect", 1));
JS_SetPropertyStr(context, object, "createTunnel", JS_NewCFunction(context, _tf_ssb_createTunnel, "createTunnel", 3));
JS_SetPropertyStr(context, object, "following", JS_NewCFunction(context, _tf_ssb_following, "following", 2));
JS_SetPropertyStr(context, object, "sync", JS_NewCFunction(context, _tf_ssb_sync, "sync", 0));
/* Write. */
JS_SetPropertyStr(context, object, "storeMessage", JS_NewCFunction(context, _tf_ssb_storeMessage, "storeMessage", 1));
JS_SetPropertyStr(context, object, "blobStore", JS_NewCFunction(context, _tf_ssb_blobStore, "blobStore", 1));

@ -2,8 +2,8 @@
#include "log.h"
#include "mem.h"
#include "ssb.h"
#include "ssb.db.h"
#include "ssb.h"
#include "util.js.h"
#include "sqlite3.h"
@ -18,7 +18,8 @@
#define _countof(a) ((int)(sizeof((a)) / sizeof(*(a))))
#endif
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);
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);
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);
@ -92,8 +93,11 @@ static void _tf_ssb_rpc_blobs_get_after_work(tf_ssb_connection_t* connection, in
const size_t k_send_max = 8192;
for (size_t offset = 0; offset < work->size; offset += k_send_max)
{
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_binary | k_ssb_rpc_flag_stream, -work->request_number, NULL, work->blob + offset,
offset + k_send_max <= work->size ? k_send_max : (work->size - offset), NULL, NULL, NULL);
if (!tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_binary | k_ssb_rpc_flag_stream, -work->request_number, NULL, work->blob + offset,
offset + k_send_max <= work->size ? k_send_max : (work->size - offset), NULL, NULL, NULL))
{
break;
}
}
tf_free(work->blob);
}
@ -106,6 +110,7 @@ static void _tf_ssb_rpc_blobs_get(tf_ssb_connection_t* connection, uint8_t flags
{
if (flags & k_ssb_rpc_flag_end_error)
{
tf_ssb_connection_remove_request(connection, -request_number);
return;
}
tf_ssb_t* ssb = tf_ssb_connection_get_ssb(connection);
@ -255,19 +260,23 @@ static void _tf_ssb_request_blob_wants_work(tf_ssb_connection_t* connection, voi
static void _tf_ssb_request_blob_wants_after_work(tf_ssb_connection_t* connection, int result, void* user_data)
{
blob_wants_work_t* work = user_data;
JSContext* context = tf_ssb_connection_get_context(connection);
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
for (int i = 0; i < work->out_id_count; i++)
if (!tf_ssb_is_shutting_down(tf_ssb_connection_get_ssb(connection)))
{
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, work->out_id[i], JS_NewInt32(context, -1));
tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
blob_wants->wants_sent++;
}
if (work->out_id_count)
{
snprintf(blob_wants->last_id, sizeof(blob_wants->last_id), "%s", work->out_id[work->out_id_count - 1]);
JSContext* context = tf_ssb_connection_get_context(connection);
tf_ssb_blob_wants_t* blob_wants = tf_ssb_connection_get_blob_wants_state(connection);
bool send_failed = false;
for (int i = 0; i < work->out_id_count && !send_failed; i++)
{
JSValue message = JS_NewObject(context);
JS_SetPropertyStr(context, message, work->out_id[i], JS_NewInt32(context, -1));
send_failed = !tf_ssb_connection_rpc_send_json(connection, k_ssb_rpc_flag_stream, -blob_wants->request_number, NULL, message, NULL, NULL, NULL);
JS_FreeValue(context, message);
blob_wants->wants_sent++;
}
if (work->out_id_count)
{
snprintf(blob_wants->last_id, sizeof(blob_wants->last_id), "%s", work->out_id[work->out_id_count - 1]);
}
}
tf_free(work);
}
@ -391,7 +400,7 @@ static void _tf_ssb_rpc_tunnel_connect(tf_ssb_connection_t* connection, uint8_t
const char* origin_str = JS_ToCString(context, origin);
const char* portal_str = JS_ToCString(context, portal);
const char* target_str = JS_ToCString(context, target);
tf_ssb_connection_tunnel_create(ssb, portal_str, -request_number, origin_str);
tf_ssb_connection_tunnel_create(ssb, portal_str, -request_number, origin_str, 0);
JS_FreeCString(context, origin_str);
JS_FreeCString(context, portal_str);
JS_FreeCString(context, target_str);
@ -765,6 +774,7 @@ typedef struct _tf_ssb_connection_send_history_stream_t
int64_t sequence;
bool keys;
bool live;
bool end_request;
bool out_finished;
int64_t out_max_sequence_seen;
@ -851,14 +861,18 @@ static void _tf_ssb_connection_send_history_stream_after_work(tf_ssb_connection_
{
for (int i = 0; i < request->out_messages_count; i++)
{
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, request->request_number, NULL, (const uint8_t*)request->out_messages[i],
strlen(request->out_messages[i]), NULL, NULL, NULL);
if (!tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_stream | k_ssb_rpc_flag_json, request->request_number, NULL, (const uint8_t*)request->out_messages[i],
strlen(request->out_messages[i]), NULL, NULL, NULL))
{
break;
}
}
if (!request->out_finished)
{
_tf_ssb_connection_send_history_stream(connection, request->request_number, request->author, request->out_max_sequence_seen, request->keys, request->live);
_tf_ssb_connection_send_history_stream(
connection, request->request_number, request->author, request->out_max_sequence_seen, request->keys, request->live, request->end_request);
}
else if (!request->live)
else if (!request->live && request->end_request)
{
tf_ssb_connection_rpc_send(connection, k_ssb_rpc_flag_json, request->request_number, NULL, (const uint8_t*)"false", strlen("false"), NULL, NULL, NULL);
}
@ -884,7 +898,8 @@ static void _tf_ssb_connection_send_history_stream_callback(tf_ssb_connection_t*
}
}
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)
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_send_history_stream_t* async = tf_malloc(sizeof(tf_ssb_connection_send_history_stream_t));
*async = (tf_ssb_connection_send_history_stream_t) {
@ -892,6 +907,7 @@ static void _tf_ssb_connection_send_history_stream(tf_ssb_connection_t* connecti
.sequence = sequence,
.keys = keys,
.live = live,
.end_request = end_request,
};
snprintf(async->author, sizeof(async->author), "%s", author);
tf_ssb_connection_schedule_idle(connection, _tf_ssb_connection_send_history_stream_callback, async);
@ -918,12 +934,12 @@ static void _tf_ssb_rpc_createHistoryStream(
JSValue keys = JS_GetPropertyStr(context, arg, "keys");
JSValue live = JS_GetPropertyStr(context, arg, "live");
bool is_keys = JS_IsUndefined(keys) || JS_ToBool(context, keys) > 0;
bool is_live = JS_ToBool(context, live) > 0;
bool is_live = JS_ToBool(context, live) > 0 && (tf_ssb_connection_get_flags(connection) & k_tf_ssb_connect_flag_one_shot) == 0;
int64_t sequence = 0;
JS_ToInt64(context, &sequence, seq);
const char* author = JS_ToCString(context, id);
_tf_ssb_connection_send_history_stream(connection, -request_number, author, sequence, is_keys, is_live);
_tf_ssb_connection_send_history_stream(connection, -request_number, author, sequence, is_keys, is_live, true);
if (is_live)
{
@ -1104,8 +1120,12 @@ static void _tf_ssb_rpc_ebt_replicate_send_messages(tf_ssb_connection_t* connect
if (sequence >= 0 && (sequence & 1) == 0)
{
int32_t request_number = tf_ssb_connection_get_ebt_request_number(connection);
_tf_ssb_connection_send_history_stream(connection, request_number, author, sequence >> 1, false, true);
tf_ssb_connection_add_new_message_request(connection, author, request_number, false);
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
{
@ -1286,6 +1306,26 @@ static void _tf_ssb_rpc_connections_changed_callback(tf_ssb_t* ssb, tf_ssb_chang
}
}
static void _tf_ssb_rpc_broadcasts_changed_visit(
const char* host, const struct sockaddr_in* addr, tf_ssb_broadcast_origin_t origin, tf_ssb_connection_t* tunnel, const uint8_t* pub, void* user_data)
{
tf_ssb_t* ssb = user_data;
if (tunnel && (tf_ssb_connection_get_flags(tunnel) & k_tf_ssb_connect_flag_one_shot) != 0 && !tf_ssb_connection_get_tunnel(tunnel))
{
char target_id[k_id_base64_len] = { 0 };
char portal_id[k_id_base64_len] = { 0 };
if (tf_ssb_id_bin_to_str(target_id, sizeof(target_id), pub) && tf_ssb_connection_get_id(tunnel, portal_id, sizeof(portal_id)))
{
tf_ssb_tunnel_create(ssb, portal_id, target_id, k_tf_ssb_connect_flag_one_shot);
}
}
}
static void _tf_ssb_rpc_broadcasts_changed_callback(tf_ssb_t* ssb, void* user_data)
{
tf_ssb_visit_broadcasts(ssb, _tf_ssb_rpc_broadcasts_changed_visit, ssb);
}
static void _tf_ssb_rpc_checkpoint(tf_ssb_t* ssb)
{
int64_t checkpoint_start_ms = uv_hrtime();
@ -1511,15 +1551,16 @@ static void _tf_ssb_rpc_peers_exchange(tf_ssb_connection_t* connection, uint8_t
void tf_ssb_rpc_register(tf_ssb_t* ssb)
{
tf_ssb_add_connections_changed_callback(ssb, _tf_ssb_rpc_connections_changed_callback, NULL, NULL);
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "gossip", "ping", NULL }, _tf_ssb_rpc_gossip_ping, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "blobs", "get", NULL }, _tf_ssb_rpc_blobs_get, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "blobs", "has", NULL }, _tf_ssb_rpc_blobs_has, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "blobs", "createWants", NULL }, _tf_ssb_rpc_blobs_createWants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "tunnel", "connect", NULL }, _tf_ssb_rpc_tunnel_connect, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "tunnel", "isRoom", NULL }, _tf_ssb_rpc_room_meta, NULL, NULL); /* FAKE-ASYNC */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "room", "metadata", NULL }, _tf_ssb_rpc_room_meta, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "room", "attendants", NULL }, _tf_ssb_rpc_room_attendants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "createHistoryStream", NULL }, _tf_ssb_rpc_createHistoryStream, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "ebt", "replicate", NULL }, _tf_ssb_rpc_ebt_replicate_server, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, (const char*[]) { "peers", "exchange", NULL }, _tf_ssb_rpc_peers_exchange, NULL, NULL); /* ASYNC */
tf_ssb_add_broadcasts_changed_callback(ssb, _tf_ssb_rpc_broadcasts_changed_callback, NULL, NULL);
tf_ssb_add_rpc_callback(ssb, "gossip.ping", _tf_ssb_rpc_gossip_ping, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, "blobs.get", _tf_ssb_rpc_blobs_get, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "blobs.has", _tf_ssb_rpc_blobs_has, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, "blobs.createWants", _tf_ssb_rpc_blobs_createWants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "tunnel.connect", _tf_ssb_rpc_tunnel_connect, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, "tunnel.isRoom", _tf_ssb_rpc_room_meta, NULL, NULL); /* FAKE-ASYNC */
tf_ssb_add_rpc_callback(ssb, "room.metadata", _tf_ssb_rpc_room_meta, NULL, NULL); /* ASYNC */
tf_ssb_add_rpc_callback(ssb, "room.attendants", _tf_ssb_rpc_room_attendants, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "createHistoryStream", _tf_ssb_rpc_createHistoryStream, NULL, NULL); /* SOURCE */
tf_ssb_add_rpc_callback(ssb, "ebt.replicate", _tf_ssb_rpc_ebt_replicate_server, NULL, NULL); /* DUPLEX */
tf_ssb_add_rpc_callback(ssb, "peers.exchange", _tf_ssb_rpc_peers_exchange, NULL, NULL); /* ASYNC */
}

@ -158,6 +158,29 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
tf_ssb_t* ssb1 = tf_ssb_create(&loop, NULL, "file:out/test_db1.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
const char* value = tf_ssb_db_get_property(ssb0, "user", "array");
assert(value == NULL);
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "1") == true);
value = tf_ssb_db_get_property(ssb0, "user", "array");
assert(strcmp(value, "[\"1\"]") == 0);
tf_free((void*)value);
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "2") == true);
value = tf_ssb_db_get_property(ssb0, "user", "array");
assert(strcmp(value, "[\"1\",\"2\"]") == 0);
tf_free((void*)value);
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "1") == false);
assert(tf_ssb_db_add_value_to_array_property(ssb0, "user", "array", "2") == false);
value = tf_ssb_db_get_property(ssb0, "user", "array");
assert(strcmp(value, "[\"1\",\"2\"]") == 0);
tf_free((void*)value);
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "1") == true);
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "1") == false);
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "2") == true);
assert(tf_ssb_db_remove_value_from_array_property(ssb0, "user", "array", "2") == false);
value = tf_ssb_db_get_property(ssb0, "user", "array");
assert(strcmp(value, "[]") == 0);
tf_free((void*)value);
uv_idle_t idle0 = { .data = ssb0 };
uv_idle_init(&loop, &idle0);
uv_idle_start(&idle0, _ssb_test_idle);
@ -245,7 +268,7 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
uint8_t id0bin[k_id_bin_len];
tf_ssb_id_str_to_bin(id0bin, id0);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
tf_printf("Waiting for connection.\n");
while (test.connection_count0 != 1 || test.connection_count1 != 1)
@ -457,8 +480,8 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
uint8_t id0bin[k_id_bin_len];
tf_ssb_id_str_to_bin(id0bin, id0);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0);
tf_printf("Waiting for connection.\n");
while (test.connection_count0 != 2 || test.connection_count1 != 1 || test.connection_count2 != 1)
@ -497,7 +520,7 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
tf_ssb_connection_rpc_send_json(connections[0], k_ssb_rpc_flag_stream | k_ssb_rpc_flag_new_request, tunnel_request_number, "tunnel.connect", message, NULL, NULL, NULL);
JS_FreeValue(context, message);
tf_ssb_connection_t* tun0 = tf_ssb_connection_tunnel_create(ssb1, id0, tunnel_request_number, id2);
tf_ssb_connection_t* tun0 = tf_ssb_connection_tunnel_create(ssb1, id0, tunnel_request_number, id2, 0);
tf_printf("tun0 = %p\n", tun0);
tf_printf("Done.\n");
@ -523,9 +546,9 @@ void tf_ssb_test_rooms(const tf_test_options_t* options)
tf_ssb_send_close(ssb1);
tf_ssb_send_close(ssb2);
tf_ssb_close_all(ssb0);
tf_ssb_close_all(ssb1);
tf_ssb_close_all(ssb2);
tf_ssb_close_all(ssb0, "end of test");
tf_ssb_close_all(ssb1, "end of test");
tf_ssb_close_all(ssb2, "end of test");
uv_run(&loop, UV_RUN_DEFAULT);
@ -689,7 +712,7 @@ void tf_ssb_test_bench(const tf_test_options_t* options)
tf_ssb_register(tf_ssb_get_context(ssb1), ssb1);
tf_ssb_server_open(ssb0, 12347);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
tf_printf("Waiting for messages.\n");
clock_gettime(CLOCK_REALTIME, &start_time);
@ -824,7 +847,7 @@ static void _ssb_test_room_broadcasts_visit(
JS_FreeValue(context, message);
tf_printf("tunnel create ssb=%p portal=%s rn=%d target=%s\n", ssb, portal, (int)tunnel_request_number, target);
tf_ssb_connection_tunnel_create(ssb, portal, tunnel_request_number, target);
tf_ssb_connection_tunnel_create(ssb, portal, tunnel_request_number, target, 0);
_break_in_a_bit(ssb, tunnel, target, tunnel_request_number);
}
}
@ -858,8 +881,8 @@ void tf_ssb_test_go_ssb_room(const tf_test_options_t* options)
tf_ssb_add_broadcasts_changed_callback(ssb0, _ssb_test_room_broadcasts_changed, NULL, NULL);
tf_ssb_connect_str(ssb0, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=");
tf_ssb_connect_str(ssb1, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=");
tf_ssb_connect_str(ssb0, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0);
tf_ssb_connect_str(ssb1, "net:linode.unprompted.com:8008~shs:Q0pc/7kXQJGIlqJxuwayL2huayzddgkVDoGkYVWQS1Y=:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=", 0);
uv_run(&loop, UV_RUN_DEFAULT);
@ -959,8 +982,8 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options)
tf_ssb_whoami(ssb0, id0, sizeof(id0));
uint8_t id0bin[k_id_bin_len];
tf_ssb_id_str_to_bin(id0bin, id0);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin);
tf_ssb_connect(ssb1, "127.0.0.1", 12347, id0bin, 0);
tf_ssb_connect(ssb2, "127.0.0.1", 12347, id0bin, 0);
while (_count_broadcasts(ssb0) != 2 || _count_broadcasts(ssb1) != 1 || _count_broadcasts(ssb2) != 1)
{
@ -980,4 +1003,40 @@ void tf_ssb_test_peer_exchange(const tf_test_options_t* options)
uv_loop_close(&loop);
}
void tf_ssb_test_publish(const tf_test_options_t* options)
{
uv_loop_t loop = { 0 };
uv_loop_init(&loop);
unlink("out/test_db0.sqlite");
tf_ssb_t* ssb = tf_ssb_create(&loop, NULL, "file:out/test_db0.sqlite", NULL);
tf_ssb_register(tf_ssb_get_context(ssb), ssb);
char id[k_id_base64_len] = { 0 };
tf_ssb_whoami(ssb, id, sizeof(id));
char executable[1024];
size_t size = sizeof(executable);
uv_exepath(executable, &size);
char command[4096];
snprintf(command, sizeof(command), "%s publish -d out/test_db0.sqlite -u :admin -i %s -c '{\"type\": \"post\", \"text\": \"One.\"}'", executable, id);
int result = system(command);
(void)result;
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
snprintf(command, sizeof(command), "%s publish -d out/test_db0.sqlite -u :admin -i %s -c '{\"type\": \"post\", \"text\": \"Two.\"}'", executable, id);
result = system(command);
assert(WIFEXITED(result));
printf("returned %d\n", WEXITSTATUS(result));
assert(WEXITSTATUS(result) == 0);
tf_ssb_destroy(ssb);
uv_run(&loop, UV_RUN_DEFAULT);
uv_loop_close(&loop);
}
#endif

@ -59,4 +59,10 @@ void tf_ssb_test_encrypt(const tf_test_options_t* options);
*/
void tf_ssb_test_peer_exchange(const tf_test_options_t* options);
/**
** Test publishing a message.
** @param options The test options.
*/
void tf_ssb_test_publish(const tf_test_options_t* options);
/** @} */

@ -27,12 +27,12 @@
#include <openssl/crypto.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <string.h>
#if !defined(_WIN32)
#include <unistd.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
#if defined(_WIN32)
@ -1530,7 +1530,7 @@ static void _tf_task_run_jobs_async(uv_async_t* async)
void tf_task_check_jobs(tf_task_t* task)
{
if (JS_IsJobPending(task->_runtime))
if (task && JS_IsJobPending(task->_runtime))
{
uv_async_send(&task->run_jobs_async);
}
@ -1869,6 +1869,7 @@ void tf_task_destroy(tf_task_t* task)
{
JSValue global = JS_GetGlobalObject(task->_context);
JS_SetPropertyStr(task->_context, global, "httpd", JS_UNDEFINED);
JS_SetPropertyStr(task->_context, global, "gProcesses", JS_NewObject(task->_context));
JS_FreeValue(task->_context, global);
}

@ -7,8 +7,8 @@
#include "task.h"
#include "util.js.h"
#include <string.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <io.h>
@ -44,7 +44,6 @@ void tf_taskstub_startup()
JS_NewClassID(&_classId);
size_t size = sizeof(_executable);
uv_exepath(_executable, &size);
tf_printf("exepath is %s\n", _executable);
initialized = true;
}
}

@ -9,8 +9,8 @@
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@ -916,6 +916,7 @@ void tf_tests(const tf_test_options_t* options)
_tf_test_run(options, "go-ssb-room", tf_ssb_test_go_ssb_room, true);
_tf_test_run(options, "encrypt", tf_ssb_test_encrypt, false);
_tf_test_run(options, "peer_exchange", tf_ssb_test_peer_exchange, false);
_tf_test_run(options, "publish", tf_ssb_test_publish, false);
tf_printf("Tests completed.\n");
#endif
}

@ -5,8 +5,8 @@
#include "task.h"
#include "tls.h"
#include <string.h>
#include <stdlib.h>
#include <string.h>
static JSClassID _classId;
static int _count;

@ -1,2 +1,2 @@
#define VERSION_NUMBER "0.0.22"
#define VERSION_NAME "Get born soon."
#define VERSION_NUMBER "0.0.24"
#define VERSION_NAME "Honey bunches of boats."

@ -25,6 +25,30 @@ try:
#options.add_argument('--headless')
driver = webdriver.Firefox(options = options, service = service)
wait = WebDriverWait(driver, 10)
driver.get('http://localhost:8888')
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'name').send_keys('adminuser')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'password').send_keys('admin_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('admin_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
driver.switch_to.default_content()
driver.get('http://localhost:8888/~core/admin/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'gs_room_name'))).send_keys('test room')
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//*[@id="gs_room_name"]/following-sibling::button'))).click()
driver.switch_to.alert.accept()
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'identity').click()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()
driver.get('http://localhost:8888')
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'login').click()
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'register_label').click()
@ -33,6 +57,53 @@ try:
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'confirm').send_keys('test_password')
driver.find_element(By.TAG_NAME, 'tf-auth').shadow_root.find_element(By.ID, 'loginButton').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.get('http://localhost:8888/~testuser/test/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
while True:
try:
wait.until(exists_in_shadow_root(driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root, By.ID, 'close_error')).click()
break
except:
pass
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.LINK_TEXT, 'edit').click()
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
editor.click()
editor.send_keys('app.setDocument("<div id=\'test-div\'>Hello, world!</div>")');
driver.find_element(By.ID, 'save').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div')))
driver.switch_to.default_content()
editor = driver.find_element(By.ID, 'editor').find_element(By.CLASS_NAME, 'cm-content')
editor.click()
editor.clear()
editor.send_keys('app.setDocument("<div id=\'test-div2\'>Hello, world, again!</div>")');
driver.find_element(By.ID, 'save').click()
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'test-div2')))
driver.switch_to.default_content()
driver.find_element(By.ID, 'delete').click()
wait.until(expected_conditions.alert_is_present()).accept()
wait.until(expected_conditions.alert_is_present()).dismiss()
driver.get('http://localhost:8888/~testuser/test/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
while True:
try:
wait.until(exists_in_shadow_root(driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root, By.ID, 'close_error')).click()
break
except:
pass
driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.LINK_TEXT, 'identity')))
@ -82,13 +153,6 @@ try:
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
id1 = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'li'))).text.split(' ')[-1]
driver.get('http://localhost:8888/~core/admin/')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
wait.until(expected_conditions.presence_of_element_located((By.ID, 'gs_room_name'))).send_keys('test room')
wait.until(expected_conditions.presence_of_element_located((By.XPATH, '//*[@id="gs_room_name"]/following-sibling::button'))).click()
driver.switch_to.alert.accept()
driver.get('http://localhost:8888')
wait.until(expected_conditions.presence_of_element_located((By.ID, 'document')))
driver.switch_to.frame(driver.find_element(By.ID, 'document'))
@ -141,7 +205,13 @@ try:
wait.until(expected_conditions.presence_of_element_located((By.ID, 'content')))
driver.switch_to.frame(wait.until(expected_conditions.presence_of_element_located((By.ID, 'document'))))
wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
# NoSuchShadowRootException
while True:
try:
tf_app = wait.until(expected_conditions.presence_of_element_located((By.TAG_NAME, 'tf-app'))).shadow_root
break
except:
pass
driver.switch_to.default_content()
driver.find_element(By.TAG_NAME, 'tf-navigation').shadow_root.find_element(By.ID, 'logout').click()

@ -3,7 +3,6 @@
if [ -z $ANDROID_NDK_ROOT ]; then
ANDROID_NDK_ROOT=~/Android/Sdk/ndk/26.1.10909125
fi
OPENSSL_VERSION=3.3.1
API_LEVEL=24
@ -11,7 +10,7 @@ BUILD_DIR=out/openssl_android_build
BUILD_TARGETS="x86_64 x86 arm64-v8a armeabi-v7a"
WORK_DIR=out/openssl-${OPENSSL_VERSION}-android
WORK_DIR=out/openssl-android
rm -rf $WORK_DIR
cp -arf deps/openssl_src/ $WORK_DIR

@ -1,14 +1,12 @@
#!/bin/bash
OPENSSL_VERSION=3.3.1
API_LEVEL=28
BUILD_DIR=out/openssl_ios_build
BUILD_TARGETS="ios64-xcrun iossimulator-xcrun"
WORK_DIR=out/openssl-${OPENSSL_VERSION}-ios
WORK_DIR=out/openssl-ios
rm -rf $WORK_DIR
cp -af deps/openssl_src/ $WORK_DIR
@ -31,7 +29,7 @@ do
case $build_target in
ios64-xcrun)
TRIBLE="arm64-darwin-ios"
OPTIONS="--static -static -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined"
OPTIONS="--static -static -ffunction-sections -fdata-sections -fPIC -Wno-macro-redefined -miphoneos-version-min=9.0"
DESTDIR="/tmp/$BUILD_DIR/arm64-ios"
SSL_TARGET="ios64-xcrun"
CC=clang

@ -1,14 +1,12 @@
#!/bin/bash
OPENSSL_VERSION=3.3.1
API_LEVEL=24
BUILD_DIR=out/openssl_mingw64_build
BUILD_TARGETS="mingw64"
WORK_DIR=out/openssl-${OPENSSL_VERSION}-mingw64
WORK_DIR=out/openssl-mingw64
rm -rf $WORK_DIR
cp -arf deps/openssl_src/ $WORK_DIR
@ -65,9 +63,9 @@ build_the_thing() {
-DOPENSSL_SMALL_FOOTPRINT"
echo "./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS" && \
./Configure $SSL_TARGET $OPTIONS $GLOBAL_OPTIONS && \
make clean && \
make build_generated && \
make libcrypto.a libssl.a || exit 128
make -s clean && \
make -s build_generated && \
make -s libcrypto.a libssl.a || exit 128
}
for build_target in $BUILD_TARGETS

@ -1,8 +1,7 @@
VERSION=3.2.0
VERSION=3.2.1
wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js -O deps/lit/lit-all.min.js
wget https://cdn.jsdelivr.net/gh/lit/dist@$VERSION/all/lit-all.min.js.map -O deps/lit/lit-all.min.js.map
cp -fv deps/lit/* apps/blog/
cp -fv deps/lit/* apps/gg/
cp -fv deps/lit/* apps/issues/
cp -fv deps/lit/* apps/journal/
cp -fv deps/lit/* apps/sneaker/