87 Commits

Author SHA1 Message Date
39abee7f73 ssb: This fixes the compose menu being clipped sometimes. Probably breaks something somewhere else.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-09-09 19:27:32 -04:00
b770619111 ssb: This fixes the attach menu being clipped. I will discover later what it breaks, I'm sure. 2025-09-09 19:20:46 -04:00
1c44857da4 core: Move register and unregister to C. 2025-09-09 19:09:37 -04:00
bca4440867 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-09-09 09:43:42 -04:00
4855543961 docs: Not meant to be versioned.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-09-03 20:33:06 -04:00
cb3d6a98b9 docs: Appease all/both of the doxygen versions.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m45s
2025-09-03 18:47:34 -04:00
ada67a13d3 update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m6s
2025-09-03 12:14:58 -04:00
f4c928f26e android: Of course you can't put null in a LinkedBlockingQueue. Shrug. Fixes a shutdown crash.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 43m31s
2025-09-02 20:57:05 -04:00
91fd515d39 android: Cleaner shutdown still.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m38s
2025-08-27 20:23:02 -04:00
be6e841d3d android: This order seems more sensible.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 20:12:20 -04:00
af6afa6903 android: Don't log from the main thread. It might block?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 19:20:02 -04:00
6ab5d2a28d build: Start work on 0.2025.9.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 18:55:24 -04:00
4be033f288 build: Do the nix dance. 2025-08-27 18:54:41 -04:00
c550f92003 docs: Fix changelog version for f-droid.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m4s
2025-08-27 18:22:23 -04:00
ed836b3ee0 build: Bump this, too.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 18:01:57 -04:00
ac7a43abf4 build: Just kidding. This is the real 0.2025.8.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-27 18:00:48 -04:00
49f19fce91 build: Let's build 0.2025.8.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m42s
2025-08-27 12:13:16 -04:00
b2197eb8e9 docs: Update the changelog. 2025-08-27 12:13:16 -04:00
5fbc2cae1c ssb: Don't indent blockquotes so much.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m51s
2025-08-24 20:35:48 -04:00
730abb49ce android: Keep the splash screen up until we're connected to our server and loaded the page. Smooths out the launch.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m38s
2025-08-22 19:24:03 -04:00
edccab054a ssb: Make the close chat button work even when a chat isn't preexisting.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m39s
2025-08-20 20:40:02 -04:00
e8210c6fdd core: Never-ending quest to fix clean shutdown.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-20 20:20:21 -04:00
55d69d7c13 test: This seems to make -t=auto more reliable. 2025-08-20 20:19:45 -04:00
50fb18d4ff core: Remove the want: log noise.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-20 19:53:18 -04:00
77b1ea1fc8 ssb: Don't show messages that were slow to load from another channel.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-20 19:38:52 -04:00
61501a9b64 ssb: Fix closing self-chat.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-20 19:28:23 -04:00
c1507adac5 docs: Start the next changelog. 2025-08-20 19:25:44 -04:00
45fb9eda1c ssb: Add a button on profiles to open a private chat.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-20 19:19:34 -04:00
982a61f4bf ssb: Remove some debug. 2025-08-20 19:15:20 -04:00
18e5b41663 ssb: Add a button to close a private chat, removing it from the sidebar. 2025-08-20 19:08:07 -04:00
910c39cbd0 update: CodeMirror. 2025-08-20 18:07:41 -04:00
9952dfd49d ssb: Fix @-completion.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m40s
2025-08-19 12:54:19 -04:00
00f75d5382 ssb: Unread status for private messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m23s
2025-08-14 12:40:58 -04:00
d78828554b ssb: Fix composing private drafts.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m55s
2025-08-13 20:28:03 -04:00
b84b561109 prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 20:14:47 -04:00
a618815500 ssb: Fix issues with private messages to one's self.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 20:12:27 -04:00
e1f3dc6ae4 ssb: Fix an issue with loading directly into private messages.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 19:58:02 -04:00
f378db6c6f ssb: Better handling of private message drafts.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 19:53:28 -04:00
7cec0f7d61 ssb: Fix private conversation keyboard alt+navigation. #125
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-08-13 19:24:25 -04:00
f902d0374c ssb: Start to break out private messages by conversation. #125 2025-08-13 19:16:34 -04:00
b5f0a0c4f7 ssb: Add support for registering for blob added notifications similarly to messages. I want to use this to load images on the fly. 2025-08-13 18:26:42 -04:00
00623cea09 android: Recompile your app with 16 KB native library alignment. 2025-08-13 18:02:20 -04:00
ed4f1d6f2c android: Be smarter about the file watcher.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m12s
2025-08-13 17:51:04 -04:00
73f4a3407f ssb: Allow showing raw messages for contact messages. 2025-08-13 12:14:31 -04:00
6f11318e84 update: speedscope 1.23.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m56s
2025-08-13 12:06:47 -04:00
e88ee91f0e ssb: More reliably load private messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m50s
2025-08-06 12:10:51 -04:00
3f8daf257c update: OpenSSL 3.5.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-08-05 12:16:03 -04:00
dc387acadc ssb: Make progress bar brighter. 2025-08-02 12:16:07 -04:00
68aa41ab96 android: Tweaking random flags until ANRs subside.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m34s
2025-08-02 12:09:08 -04:00
85b23437b3 docs: Fix all the TODOCS. #39
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m17s
2025-08-02 09:07:45 -04:00
c59fba817d ssb: Show the progress indicator more consistently.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m54s
2025-07-31 12:48:45 -04:00
c3415ab75c docs: Expose the rest of core to docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m24s
2025-07-30 20:25:20 -04:00
f1d0151d71 ssb: Make the progress bar more indefinite-looking.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 20:04:34 -04:00
3c5c1756d1 ssb: A progress bar experiment. 2025-07-30 19:49:08 -04:00
6a6b65d1b3 build: Update nix config. Start building 0.2025.8, switching to calver.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-30 19:26:26 -04:00
81bd54dbe6 build: Let's build 0.0.33.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 29m24s
2025-07-30 18:21:17 -04:00
6a1bb0d3bc update: sqlite 3.50.4. 2025-07-30 17:47:06 -04:00
705e8b553f docs: Expose the rest of the core js to doxygen.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m35s
2025-07-27 21:48:18 -04:00
e4729b22f2 docs: Hook up doxygen to some of the core JS. Now maybe I am slightly incentivized to make progress on #39.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m18s
2025-07-27 15:04:17 -04:00
662112551a core: Remove/clean up some unhelpful logging. #124
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m9s
2025-07-27 14:06:46 -04:00
38fe88aab8 android: Guard aganst ANRs harder?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-07-27 13:17:49 -04:00
578c51faa0 update: npm.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m46s
2025-07-27 12:42:52 -04:00
a3ccc73b81 core: Move code.apps() to C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m44s
2025-07-18 11:53:31 -04:00
7312f4d43a httpd: Oops. 2025-07-18 11:53:31 -04:00
8b546c7e02 welcome: Add a gitea icon.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m58s
2025-07-18 10:10:16 -04:00
c0b6ff2e64 update: sqlite 3.50.3.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m12s
2025-07-17 20:57:10 -04:00
638b7cc1e5 ssb: Slight suggested follows cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m26s
2025-07-16 21:01:24 -04:00
05e54e1be0 httpd: More minor cleanup.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-16 20:44:42 -04:00
4c3299ead0 core: Begin to split some of the largest modules into smaller pieces, starting with HTTP endpoints.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m13s
2025-07-16 20:12:27 -04:00
1ef56b35ad update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m50s
2025-07-16 18:59:10 -04:00
061e79c295 welcome: Let's try this without server-side JS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m51s
2025-07-16 17:57:15 -04:00
5edfe732b1 core: Make it possible to host a web page with no additional server-side JS. An experiment in supporting simplicity and increased ability to be publicly searched and archived.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 30m55s
2025-07-16 12:47:16 -04:00
a8f9b67f71 cleanup: Remove the /ebt endpoint. The connections tab is superior.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m8s
2025-07-15 18:18:35 -04:00
de7fbf1eb7 update: picohttpparser.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m43s
2025-07-14 21:10:02 -04:00
a51a3d7e43 update: lit 3.3.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m49s
2025-07-11 21:36:28 -04:00
433b3b1003 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m18s
2025-07-09 18:59:03 -04:00
6703c5b584 ssb: Fix letterboxing/pillarboxing of images regardless of aspect ratio.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:53:27 -04:00
5f729efabe ssb: Load more messages at a time.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:35:51 -04:00
b2085b3f28 ssb: Try to keep the profile description from falling off the page. CSS, ugg. #126
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-09 18:25:47 -04:00
2f893494b0 ssb: Show the number of accounts followed on the profile show/hide followed button.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m51s
2025-07-09 17:50:06 -04:00
e26af21f63 ssb: Disambiguate some sqlite errors better. Today I learned there are various cases it doesn't update the error message, and prepare can succeed and not produce a statement.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m9s
2025-07-09 12:36:58 -04:00
7e1d738f8d ssb: Don't show the connection buttons in the main bar if there's a sidebar. 2025-07-09 12:09:03 -04:00
199448e11e build: Back to building 0.0.33-wip.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 31m35s
2025-07-07 12:40:32 -04:00
fdaabab807 android: Suppress a setDatabaseEnabled deprecation warning.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-07-07 12:12:37 -04:00
ca4560c5c9 update: speedscope 1.23.0. 2025-07-07 12:11:59 -04:00
2478f3064d android: Don't draw behind the status bar.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 32m5s
2025-07-06 07:39:09 -04:00
e9b8b43e7c ssb: Minor formatting around the connection sidebar buttons. 2025-07-06 07:27:01 -04:00
75 changed files with 3924 additions and 2790 deletions

365
Doxyfile
View File

@@ -1,4 +1,4 @@
# Doxyfile 1.9.8 # Doxyfile 1.9.4
# This file describes the settings to be used by the documentation system # This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project. # doxygen (www.doxygen.org) for a project.
@@ -19,8 +19,7 @@
# configuration file: # configuration file:
# doxygen -x [configFile] # doxygen -x [configFile]
# Use doxygen to compare the used configuration file with the template # Use doxygen to compare the used configuration file with the template
# configuration file without replacing the environment variables or CMake type # configuration file without replacing the environment variables:
# replacement variables:
# doxygen -x_noenv [configFile] # doxygen -x_noenv [configFile]
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -86,7 +85,7 @@ CREATE_SUBDIRS = NO
# level increment doubles the number of directories, resulting in 4096 # level increment doubles the number of directories, resulting in 4096
# directories at level 8 which is the default and also the maximum value. The # directories at level 8 which is the default and also the maximum value. The
# sub-directories are organized in 2 levels, the first level always has a fixed # sub-directories are organized in 2 levels, the first level always has a fixed
# number of 16 directories. # numer of 16 directories.
# Minimum value: 0, maximum value: 8, default value: 8. # Minimum value: 0, maximum value: 8, default value: 8.
# This tag requires that the tag CREATE_SUBDIRS is set to YES. # This tag requires that the tag CREATE_SUBDIRS is set to YES.
@@ -342,7 +341,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
# #
# Note see also the list of default file extension mappings. # Note see also the list of default file extension mappings.
EXTENSION_MAPPING = EXTENSION_MAPPING = js=javascript
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable # according to the Markdown format, which allows for more readable
@@ -363,17 +362,6 @@ MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5 TOC_INCLUDE_HEADINGS = 5
# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
# generate identifiers for the Markdown headings. Note: Every identifier is
# unique.
# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
# sequence number starting at 0 and GITHUB use the lower case version of title
# with any whitespace replaced by '-' and punctuation characters removed.
# The default value is: DOXYGEN.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
MARKDOWN_ID_STYLE = DOXYGEN
# When enabled doxygen tries to link words that correspond to documented # When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can # classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or # be prevented in individual cases by putting a % sign in front of the word or
@@ -498,14 +486,6 @@ LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1 NUM_PROC_THREADS = 1
# If the TIMESTAMP tag is set different from NO then each generated page will
# contain the date or date and time when the page was generated. Setting this to
# NO can help when comparing the output of multiple runs.
# Possible values are: YES, NO, DATETIME and DATE.
# The default value is: NO.
TIMESTAMP = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Build related configuration options # Build related configuration options
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -587,8 +567,7 @@ HIDE_UNDOC_MEMBERS = NO
# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
# undocumented classes that are normally visible in the class hierarchy. If set # undocumented classes that are normally visible in the class hierarchy. If set
# to NO, these classes will be included in the various overviews. This option # to NO, these classes will be included in the various overviews. This option
# will also hide undocumented C++ concepts if enabled. This option has no effect # has no effect if EXTRACT_ALL is enabled.
# if EXTRACT_ALL is enabled.
# The default value is: NO. # The default value is: NO.
HIDE_UNDOC_CLASSES = NO HIDE_UNDOC_CLASSES = NO
@@ -626,8 +605,7 @@ INTERNAL_DOCS = NO
# Windows (including Cygwin) and MacOS, users should typically set this option # Windows (including Cygwin) and MacOS, users should typically set this option
# to NO, whereas on Linux or other Unix flavors it should typically be set to # to NO, whereas on Linux or other Unix flavors it should typically be set to
# YES. # YES.
# Possible values are: SYSTEM, NO and YES. # The default value is: system dependent.
# The default value is: SYSTEM.
CASE_SENSE_NAMES = YES CASE_SENSE_NAMES = YES
@@ -879,26 +857,11 @@ WARN_IF_INCOMPLETE_DOC = YES
WARN_NO_PARAMDOC = NO WARN_NO_PARAMDOC = NO
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
# undocumented enumeration values. If set to NO, doxygen will accept
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
# The default value is: NO.
WARN_IF_UNDOC_ENUM_VAL = NO
# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status. # at the end of the doxygen process doxygen will return with a non-zero status.
# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves # Possible values are: NO, YES and FAIL_ON_WARNINGS.
# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
# write the warning messages in between other messages but write them at the end
# of a run, in case a WARN_LOGFILE is defined the warning messages will be
# besides being in the defined file also be shown at the end of a run, unless
# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
# the behavior will remain as with the setting FAIL_ON_WARNINGS.
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO. # The default value is: NO.
WARN_AS_ERROR = NO WARN_AS_ERROR = NO
@@ -944,6 +907,11 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched. # Note: If this tag is empty the current directory is searched.
INPUT = README.md \ INPUT = README.md \
core/app.js \
core/client.js \
core/core.js \
core/http.js \
core/tfrpc.js \
docs/ \ docs/ \
src/ src/
@@ -952,21 +920,10 @@ INPUT = README.md \
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv # libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see: # documentation (see:
# https://www.gnu.org/software/libiconv/) for the list of possible encodings. # https://www.gnu.org/software/libiconv/) for the list of possible encodings.
# See also: INPUT_FILE_ENCODING
# The default value is: UTF-8. # The default value is: UTF-8.
INPUT_ENCODING = UTF-8 INPUT_ENCODING = UTF-8
# This tag can be used to specify the character encoding of the source files
# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
# character encoding on a per file pattern basis. Doxygen will compare the file
# name with each pattern and apply the encoding instead of the default
# INPUT_ENCODING) if there is a match. The character encodings are a list of the
# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
# "INPUT_ENCODING" for further information on supported encodings.
INPUT_FILE_ENCODING =
# If the value of the INPUT tag contains directories, you can use the # If the value of the INPUT tag contains directories, you can use the
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
# *.h) to filter out the source-files in the directories. # *.h) to filter out the source-files in the directories.
@@ -978,14 +935,15 @@ INPUT_FILE_ENCODING =
# Note the list of default checked file patterns might differ from the list of # Note the list of default checked file patterns might differ from the list of
# default file extension mappings. # default file extension mappings.
# #
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, # *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, # *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be # *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. # *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.h \ FILE_PATTERNS = *.h \
*.js \
*.md *.md
# The RECURSIVE tag can be used to specify whether or not subdirectories should # The RECURSIVE tag can be used to specify whether or not subdirectories should
@@ -1024,6 +982,9 @@ EXCLUDE_PATTERNS =
# output. The symbol name can be a fully qualified name, a word, or if the # output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass, # wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test # ANamespace::AClass, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS = EXCLUDE_SYMBOLS =
@@ -1068,11 +1029,6 @@ IMAGE_PATH = docs/images/
# code is scanned, but not when the output code is generated. If lines are added # code is scanned, but not when the output code is generated. If lines are added
# or removed, the anchors will not be placed correctly. # or removed, the anchors will not be placed correctly.
# #
# Note that doxygen will use the data processed and written to standard output
# for further processing, therefore nothing else, like debug statements or used
# commands (so in case of a Windows batch file always use @echo OFF), should be
# written to standard output.
#
# Note that for custom extensions or not directly supported extensions you also # Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not # need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by doxygen. # properly processed by doxygen.
@@ -1114,15 +1070,6 @@ FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE = README.md USE_MDFILE_AS_MAINPAGE = README.md
# The Fortran standard specifies that for fixed formatted Fortran code all
# characters from position 72 are to be considered as comment. A common
# extension is to allow longer lines before the automatic comment starts. The
# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
# be processed before the automatic comment starts.
# Minimum value: 7, maximum value: 10000, default value: 72.
FORTRAN_COMMENT_AFTER = 72
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to source browsing # Configuration options related to source browsing
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -1260,11 +1207,10 @@ CLANG_DATABASE_PATH =
ALPHABETICAL_INDEX = YES ALPHABETICAL_INDEX = YES
# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) # In case all classes in a project start with a common prefix, all classes will
# that should be ignored while generating the index headers. The IGNORE_PREFIX # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
# tag works for classes, function and member names. The entity will be placed in # can be used to specify a prefix (or a list of prefixes) that should be ignored
# the alphabetical list under the first letter of the entity name that remains # while generating the index headers.
# after removing the prefix.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
IGNORE_PREFIX = IGNORE_PREFIX =
@@ -1343,12 +1289,7 @@ HTML_STYLESHEET =
# Doxygen will copy the style sheet files to the output directory. # Doxygen will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last # Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the # style sheet in the list overrules the setting of the previous ones in the
# list). # list). For an example see the documentation.
# Note: Since the styling of scrollbars can currently not be overruled in
# Webkit/Chromium, the styling will be left out of the default doxygen.css if
# one or more extra stylesheets have been specified. So if scrollbar
# customization is desired it has to be added explicitly. For an example see the
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES. # This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET = HTML_EXTRA_STYLESHEET =
@@ -1363,19 +1304,6 @@ HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES = HTML_EXTRA_FILES =
# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
# should be rendered with a dark or light theme.
# Possible values are: LIGHT always generate light mode output, DARK always
# generate dark mode output, AUTO_LIGHT automatically set the mode according to
# the user preference, use light mode if no preference is set (the default),
# AUTO_DARK automatically set the mode according to the user preference, use
# dark mode if no preference is set and TOGGLE allow to user to switch between
# light and dark mode via a button.
# The default value is: AUTO_LIGHT.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE = AUTO_LIGHT
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to # will adjust the colors in the style sheet and background images according to
# this color. Hue is specified as an angle on a color-wheel, see # this color. Hue is specified as an angle on a color-wheel, see
@@ -1406,6 +1334,15 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80 HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to YES can help to show when doxygen was last run and thus if the
# documentation is up to date.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
#HTML_TIMESTAMP = NO
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that # documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will # are dynamically created via JavaScript. If disabled, the navigation index will
@@ -1425,13 +1362,6 @@ HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO HTML_DYNAMIC_SECTIONS = NO
# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
# dynamically folded and expanded in the generated HTML source code.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_CODE_FOLDING = YES
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand # shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to # and collapse entries dynamically later on. Doxygen will expand the tree to
@@ -1562,16 +1492,6 @@ BINARY_TOC = NO
TOC_EXPAND = NO TOC_EXPAND = NO
# The SITEMAP_URL tag is used to specify the full URL of the place where the
# generated documentation will be placed on the server by the user during the
# deployment of the documentation. The generated sitemap is called sitemap.xml
# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
# is specified no sitemap is generated. For information about the sitemap
# protocol see https://www.sitemaps.org
# This tag requires that the tag GENERATE_HTML is set to YES.
SITEMAP_URL =
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@@ -1747,6 +1667,17 @@ HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10 FORMULA_FONTSIZE = 10
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
# generated for formulas are transparent PNGs. Transparent PNGs are not
# supported properly for IE 6.0, but are supported on all modern browsers.
#
# Note that when changing this option you need to delete any form_*.png files in
# the HTML output directory before the changes have effect.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
#FORMULA_TRANSPARENT = YES
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
# to create new LaTeX commands to be used in formulas as building blocks. See # to create new LaTeX commands to be used in formulas as building blocks. See
# the section "Including formulas" for details. # the section "Including formulas" for details.
@@ -1808,8 +1739,8 @@ MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example # extension names that should be enabled during MathJax rendering. For example
# for MathJax version 2 (see # for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html
# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): # #tex-and-latex-extensions):
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# For example for MathJax version 3 (see # For example for MathJax version 3 (see
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): # http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
@@ -2060,16 +1991,9 @@ PDF_HYPERLINKS = YES
USE_PDFLATEX = YES USE_PDFLATEX = YES
# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch # command to the generated LaTeX files. This will instruct LaTeX to keep running
# mode nothing is printed on the terminal, errors are scrolled as if <return> is # if errors occur, instead of asking the user for help.
# hit at every error; missing files that TeX tries to input or request from
# keyboard input (\read on a not open input stream) cause the job to abort,
# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
# but there is no possibility of user interaction just like in batch mode,
# SCROLL In scroll mode, TeX will stop only for missing files to input or if
# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
# each error, asking for user intervention.
# The default value is: NO. # The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES. # This tag requires that the tag GENERATE_LATEX is set to YES.
@@ -2090,6 +2014,14 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain LATEX_BIB_STYLE = plain
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
#LATEX_TIMESTAMP = NO
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered, # path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the # it will be relative to the LATEX_OUTPUT directory. If left blank the
@@ -2255,39 +2187,13 @@ DOCBOOK_OUTPUT = docbook
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature # the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment. # is still experimental and incomplete at the moment.
# The default value is: NO. # The default value is: NO.
GENERATE_AUTOGEN_DEF = NO GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
# database with symbols found by doxygen stored in tables.
# The default value is: NO.
#GENERATE_SQLITE3 = NO
# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
# in front of it.
# The default directory is: sqlite3.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
#SQLITE3_OUTPUT = sqlite3
# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
# database file will be recreated with each doxygen run. If set to NO, doxygen
# will warn if an a database file is already found and not modify it.
# The default value is: YES.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
#SQLITE3_RECREATE_DB = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the Perl module output # Configuration options related to the Perl module output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -2430,15 +2336,15 @@ TAGFILES =
GENERATE_TAGFILE = GENERATE_TAGFILE =
# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces # If the ALLEXTERNALS tag is set to YES, all external class will be listed in
# will be listed in the class and namespace index. If set to NO, only the # the class index. If set to NO, only the inherited external classes will be
# inherited external classes will be listed. # listed.
# The default value is: NO. # The default value is: NO.
ALLEXTERNALS = NO ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the topic index. If set to NO, only the current project's groups will be # in the modules index. If set to NO, only the current project's groups will be
# listed. # listed.
# The default value is: YES. # The default value is: YES.
@@ -2452,9 +2358,16 @@ EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES EXTERNAL_PAGES = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to diagram generator tools # Configuration options related to the dot tool
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
DIA_PATH =
# If set to YES the inheritance and collaboration graphs will hide inheritance # If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class. # and usage relations if the target is undocumented or is not a class.
# The default value is: YES. # The default value is: YES.
@@ -2463,10 +2376,10 @@ HIDE_UNDOC_RELATIONS = YES
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see: # available from the path. This tool is part of Graphviz (see:
# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is # Bell Labs. The other options in this section have no effect if this option is
# set to NO # set to NO
# The default value is: YES. # The default value is: NO.
HAVE_DOT = YES HAVE_DOT = YES
@@ -2480,51 +2393,37 @@ HAVE_DOT = YES
DOT_NUM_THREADS = 0 DOT_NUM_THREADS = 0
# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of # When you want a differently looking font in the dot files that doxygen
# subgraphs. When you want a differently looking font in the dot files that # generates you can specify the font name using DOT_FONTNAME. You need to make
# doxygen generates you can specify fontname, fontcolor and fontsize attributes. # sure dot is able to find the font, which can be done by putting it in a
# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node, # standard location or by setting the DOTFONTPATH environment variable or by
# Edge and Graph Attributes specification</a> You need to make sure dot is able # setting DOT_FONTPATH to the directory containing the font.
# to find the font, which can be done by putting it in a standard location or by # The default value is: Helvetica.
# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
# directory containing the font. Default graphviz fontsize is 14.
# The default value is: fontname=Helvetica,fontsize=10.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" #DOT_FONTNAME = Helvetica
# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can # The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a # dot graphs.
# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about # Minimum value: 4, maximum value: 24, default value: 10.
# arrows shapes.</a>
# The default value is: labelfontname=Helvetica,labelfontsize=10.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" #DOT_FONTSIZE = 10
# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes # By default doxygen will tell dot to use the default font as specified with
# around nodes set 'shape=plain' or 'shape=plaintext' <a # DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a> # the path where dot can find it using this tag.
# The default value is: shape=box,height=0.2,width=0.4.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" #DOT_FONTPATH =
# You can set the path where dot can find font specified with fontname in # If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
# DOT_COMMON_ATTR and others dot attributes. # graph for each documented class showing the direct and indirect inheritance
# This tag requires that the tag HAVE_DOT is set to YES. # relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
DOT_FONTPATH = # to TEXT the direct and indirect inheritance relations will be shown as texts /
# links.
# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will # Possible values are: NO, YES, TEXT and GRAPH.
# generate a graph for each documented class showing the direct and indirect
# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
# relations will be shown as texts / links.
# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
# The default value is: YES. # The default value is: YES.
CLASS_GRAPH = YES CLASS_GRAPH = YES
@@ -2532,21 +2431,15 @@ CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation # graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the # dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes. Explicit enabling a collaboration graph, # class with other documented classes.
# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
# command \collaborationgraph. Disabling a collaboration graph can be
# accomplished by means of the command \hidecollaborationgraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies. Explicit enabling a group # groups, showing the direct groups dependencies. See also the chapter Grouping
# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means # in the manual.
# of the command \groupgraph. Disabling a directory graph can be accomplished by
# means of the command \hidegroupgraph. See also the chapter Grouping in the
# manual.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2606,9 +2499,7 @@ TEMPLATE_RELATIONS = NO
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the # YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented # direct and indirect include dependencies of the file with other documented
# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, # files.
# can be accomplished by means of the command \includegraph. Disabling an
# include graph can be accomplished by means of the command \hideincludegraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2617,10 +2508,7 @@ INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing # set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented # the direct and indirect include dependencies of the file with other documented
# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set # files.
# to NO, can be accomplished by means of the command \includedbygraph. Disabling
# an included by graph can be accomplished by means of the command
# \hideincludedbygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2660,10 +2548,7 @@ GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The # dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the # dependency relations are determined by the #include relations between the
# files in the directories. Explicit enabling a directory graph, when # files in the directories.
# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
# \directorygraph. Disabling a directory graph can be accomplished by means of
# the command \hidedirectorygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2679,13 +2564,12 @@ DIR_GRAPH_MAX_DEPTH = 1
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section # generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see: # output formats in the documentation of the dot tool (Graphviz (see:
# https://www.graphviz.org/)). # http://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this # to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement). # requirement).
# Possible values are: png, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, # Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
# gif, gif:cairo, gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, # png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# png:cairo, png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# png:gdiplus:gdiplus. # png:gdiplus:gdiplus.
# The default value is: png. # The default value is: png.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2717,12 +2601,11 @@ DOT_PATH =
DOTFILE_DIRS = DOTFILE_DIRS =
# You can include diagrams made with dia in doxygen documentation. Doxygen will # The MSCFILE_DIRS tag can be used to specify one or more directories that
# then run dia to produce the diagram and insert it in the documentation. The # contain msc files that are included in the documentation (see the \mscfile
# DIA_PATH tag allows you to specify the directory where the dia binary resides. # command).
# If left empty dia is assumed to be found in the default search path.
DIA_PATH = MSCFILE_DIRS =
# The DIAFILE_DIRS tag can be used to specify one or more directories that # The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile # contain dia files that are included in the documentation (see the \diafile
@@ -2772,6 +2655,18 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0 MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
# background. This is disabled by default, because dot on Windows does not seem
# to support this out of the box.
#
# Warning: Depending on the platform used, enabling this option may lead to
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
# read).
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
#DOT_TRANSPARENT = NO
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This # files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support # makes dot run faster, but since only newer versions of dot (>1.8.10) support
@@ -2799,19 +2694,3 @@ GENERATE_LEGEND = YES
# The default value is: YES. # The default value is: YES.
DOT_CLEANUP = YES DOT_CLEANUP = YES
# You can define message sequence charts within doxygen comments using the \msc
# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
# use a built-in version of mscgen tool to produce the charts. Alternatively,
# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
# specifying prog as the value, doxygen will call the tool as prog -T
# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
# output file formats "png", "eps", "svg", and "ismap".
MSCGEN_TOOL =
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
MSCFILE_DIRS =

View File

@@ -16,14 +16,14 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker. ## LD := Linker.
## ANDROID_SDK := Path to the Android SDK. ## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 39 VERSION_CODE := 43
VERSION_CODE_IOS := 15 VERSION_CODE_IOS := 17
VERSION_NUMBER := 0.0.32.1 VERSION_NUMBER := 0.2025.9-wip
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0 IPHONEOS_VERSION_MIN=14.0
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500200.zip SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500400.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
@@ -253,7 +253,10 @@ $(ANDROID_TARGETS): CFLAGS += \
-fno-asynchronous-unwind-tables \ -fno-asynchronous-unwind-tables \
-funwind-tables \ -funwind-tables \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC $(ANDROID_TARGETS): LDFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-Wl,-z,max-page-size=16384 \
-fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og $(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og $(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \ $(RELEASE_TARGETS): CFLAGS += \
@@ -1140,6 +1143,11 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity @adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo .PHONY: releaseapkgo
x86releaseapkgo: out/TildeFriends-x86-release.apk ## Build, install, and run an x86 release Android APK.
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: x86releaseapkgo
apklog: ## Display Android log output. apklog: ## Display Android log output.
@adb logcat *:S tildefriends @adb logcat *:S tildefriends
.PHONY: apklog .PHONY: apklog

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

View File

@@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🦀",
"previous": "&Ym1vefMN4CV4UIgLuV+zu52qj58WwIScctt4v5YIHmQ=.sha256" "previous": "&C4r2tB/tzPjPSrS3NRiebaNOP94G4FX80yIg7YN9sBQ=.sha256"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -21,10 +21,13 @@ class TfElement extends LitElement {
channels_latest: {type: Object}, channels_latest: {type: Object},
guest: {type: Boolean}, guest: {type: Boolean},
url: {type: String}, url: {type: String},
private_closed: {type: Object},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
is_administrator: {type: Boolean}, is_administrator: {type: Boolean},
stay_connected: {type: Boolean}, stay_connected: {type: Boolean},
progress: {type: Number},
}; };
} }
@@ -47,6 +50,7 @@ class TfElement extends LitElement {
this.loading_latest = 0; this.loading_latest = 0;
this.loading_latest_scheduled = 0; this.loading_latest_scheduled = 0;
this.recent_reactions = []; this.recent_reactions = [];
this.private_closed = {};
tfrpc.rpc.getBroadcasts().then((b) => { tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || []; self.broadcasts = b || [];
}); });
@@ -56,6 +60,7 @@ class TfElement extends LitElement {
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash)); tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) { tfrpc.register(function hashChanged(hash) {
self.set_hash(hash); self.set_hash(hash);
self.reset_progress();
}); });
tfrpc.register(async function notifyNewMessage(id) { tfrpc.register(async function notifyNewMessage(id) {
await self.fetch_new_message(id); await self.fetch_new_message(id);
@@ -83,9 +88,22 @@ class TfElement extends LitElement {
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length; this.guest = !this.whoami?.length;
this.ids = ids; this.ids = ids;
let private_closed =
(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}';
this.private_closed = JSON.parse(private_closed);
await this.load_channels(); await this.load_channels();
} }
async close_private_chat(event) {
let update = {};
update[event.detail.key] = true;
this.private_closed = Object.assign(update, this.private_closed);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
);
}
async load_channels() { async load_channels() {
let channels = await tfrpc.rpc.query( let channels = await tfrpc.rpc.query(
` `
@@ -133,12 +151,32 @@ class TfElement extends LitElement {
} }
} }
visible_private() {
if (!this.grouped_private_messages || !this.private_closed) {
return [];
}
let self = this;
return Object.fromEntries(
Object.entries(this.grouped_private_messages).filter(([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
})
);
}
next_channel(delta) { next_channel(delta) {
let channel_names = [ let channel_names = [
'', '',
'@', '@',
'👍', '👍',
'🔐', ...Object.keys(this.visible_private())
.sort()
.map((x) => '🔐' + JSON.parse(x).join(',')),
...this.channels.map((x) => '#' + x), ...this.channels.map((x) => '#' + x),
]; ];
let index = channel_names.indexOf(this.hash.substring(1)); let index = channel_names.indexOf(this.hash.substring(1));
@@ -364,6 +402,36 @@ class TfElement extends LitElement {
return result; return result;
} }
async group_private_messages(messages) {
let groups = {};
let result = await this.decrypt(
await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, author, timestamp, json(content) AS content
FROM messages
JOIN json_each(?) AS ids
WHERE messages.id = ids.value
ORDER BY timestamp DESC
`,
[JSON.stringify(messages)]
)
);
for (let message of result) {
let key = JSON.stringify(
[
...new Set(
message?.decrypted?.recps?.filter((x) => x != this.whoami)
),
].sort() ?? []
);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(message);
}
return groups;
}
async load_channels_latest(following) { async load_channels_latest(following) {
let start_time = new Date(); let start_time = new Date();
let latest_private = this.get_latest_private(following); let latest_private = this.get_latest_private(following);
@@ -436,12 +504,15 @@ class TfElement extends LitElement {
console.log('channels took', (new Date() - start_time) / 1000.0); console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this; let self = this;
start_time = new Date(); start_time = new Date();
latest_private.then(function (latest) { latest_private.then(async function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, { self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0], '🔐': latest[0],
}); });
console.log('private took', (new Date() - start_time) / 1000.0); console.log('private took', (new Date() - start_time) / 1000.0);
self.private_messages = latest[1]; self.private_messages = latest[1];
self.grouped_private_messages = await self.group_private_messages(
latest[1]
);
}); });
} }
@@ -450,7 +521,28 @@ class TfElement extends LitElement {
this.schedule_load_latest(); this.schedule_load_latest();
} }
reset_progress() {
if (this.progress === undefined) {
this._progress_start = new Date();
requestAnimationFrame(this.update_progress.bind(this));
}
}
update_progress() {
if (
!this.loading_latest &&
!this.loading_latest_scheduled &&
!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
) {
this.progress = undefined;
return;
}
this.progress = (new Date() - this._progress_start).valueOf();
requestAnimationFrame(this.update_progress.bind(this));
}
schedule_load_latest() { schedule_load_latest() {
this.reset_progress();
if (!this.loading_latest) { if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest(); this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load(); this.load();
@@ -495,6 +587,7 @@ class TfElement extends LitElement {
async load() { async load() {
this.loading_latest = true; this.loading_latest = true;
this.reset_progress();
try { try {
let start_time = new Date(); let start_time = new Date();
let whoami = this.whoami; let whoami = this.whoami;
@@ -603,8 +696,11 @@ class TfElement extends LitElement {
@channelsetunread=${this.channel_set_unread} @channelsetunread=${this.channel_set_unread}
@refresh=${this.refresh} @refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected} @toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
@closeprivatechat=${this.close_private_chat}
.connections=${this.connections} .connections=${this.connections}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.grouped_private_messages=${this.visible_private()}
.recent_reactions=${this.recent_reactions} .recent_reactions=${this.recent_reactions}
?is_administrator=${this.is_administrator} ?is_administrator=${this.is_administrator}
?stay_connected=${this.stay_connected} ?stay_connected=${this.stay_connected}
@@ -646,6 +742,7 @@ class TfElement extends LitElement {
async set_tab(tab) { async set_tab(tab) {
this.tab = tab; this.tab = tab;
if (tab === 'news') { if (tab === 'news') {
this.schedule_load_latest();
await tfrpc.rpc.setHash('#'); await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') { } else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections'); await tfrpc.rpc.setHash('#connections');
@@ -692,7 +789,7 @@ class TfElement extends LitElement {
class="w3-bar w3-theme-l1" class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10" style="position: static; top: 0; z-index: 10"
> >
${this.is_administrator ${this.is_administrator && self.tab != 'news'
? html` ? html`
<button <button
class=${'w3-bar-item w3-button w3-circle w3-ripple' + class=${'w3-bar-item w3-button w3-circle w3-ripple' +
@@ -751,11 +848,23 @@ class TfElement extends LitElement {
Loading... Loading...
</div>` </div>`
: this.render_tab(); : this.render_tab();
let progress =
this.progress !== undefined
? html`
<div style="position: absolute; width: 100%" id="progress">
<div
class="w3-theme-l3"
style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
></div>
</div>
`
: undefined;
return html` return html`
<div <div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark" class="w3-theme-dark"
> >
${progress}
<div style="flex: 0 0">${tabs}</div> <div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout"> <div style="flex: 1 1; overflow: auto; contain: layout">
${contents} ${contents}

View File

@@ -16,6 +16,7 @@ class TfComposeElement extends LitElement {
author: {type: String}, author: {type: String},
channel: {type: String}, channel: {type: String},
new_thread: {type: Boolean}, new_thread: {type: Boolean},
recipients: {type: Array},
}; };
} }
@@ -91,7 +92,9 @@ class TfComposeElement extends LitElement {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { detail: {
id: this.branch, id:
this.branch ??
(this.recipients ? this.recipients.join(',') : undefined),
draft: draft, draft: draft,
}, },
}) })
@@ -291,7 +294,7 @@ class TfComposeElement extends LitElement {
} }
} }
firstUpdated() { get_values() {
let values = Object.entries(this.users).map((x) => ({ let values = Object.entries(this.users).map((x) => ({
key: x[1].name ?? x[0], key: x[1].name ?? x[0],
value: x[0], value: x[0],
@@ -307,11 +310,15 @@ class TfComposeElement extends LitElement {
values values
); );
} }
return values;
}
firstUpdated() {
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot, iframe: this.shadowRoot,
collection: [ collection: [
{ {
values: values, values: this.get_values(),
selectTemplate: function (item) { selectTemplate: function (item) {
return item return item
? `[@${item.original.key}](${item.original.value})` ? `[@${item.original.key}](${item.original.value})`
@@ -330,6 +337,7 @@ class TfComposeElement extends LitElement {
], ],
}); });
tribute.attach(this.renderRoot.getElementById('edit')); tribute.attach(this.renderRoot.getElementById('edit'));
this._tribute = tribute;
} }
updated() { updated() {
@@ -340,6 +348,7 @@ class TfComposeElement extends LitElement {
preview.innerHTML = this.process_text(edit.innerText); preview.innerHTML = this.process_text(edit.innerText);
this.last_updated_text = edit.innerText; this.last_updated_text = edit.innerText;
} }
this._tribute.collection[0].values = this.get_values();
let encrypt = this.renderRoot.getElementById('encrypt_to'); let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) { if (encrypt) {
let tribute = new Tribute({ let tribute = new Tribute({
@@ -496,7 +505,17 @@ class TfComposeElement extends LitElement {
} }
get_draft() { get_draft() {
return this.drafts[this.branch || ''] || {}; let key =
this.branch ||
(this.recipients ? this.recipients.join(',') : undefined) ||
'';
let draft = this.drafts[key] || {};
if (this.recipients && !draft.encrypt_to?.length) {
draft.encrypt_to = [
...new Set(this.recipients).union(new Set(draft.encrypt_to ?? [])),
];
}
return draft;
} }
update_encrypt(event) { update_encrypt(event) {
@@ -606,7 +625,7 @@ class TfComposeElement extends LitElement {
<div class="w3-half"> <div class="w3-half">
<span <span
class="w3-input w3-theme-d1 w3-border" class="w3-input w3-theme-d1 w3-border"
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap" style="resize: vertical; width: 100%; white-space: pre-wrap"
placeholder="Write a post here." placeholder="Write a post here."
id="edit" id="edit"
@input=${this.input} @input=${this.input}

View File

@@ -191,12 +191,12 @@ class TfMessageElement extends LitElement {
div.style.display = 'grid'; div.style.display = 'grid';
let img = document.createElement('img'); let img = document.createElement('img');
img.src = link; img.src = link;
img.style.maxWidth = '100%'; img.style.maxWidth = '100vw';
img.style.maxHeight = '100%'; img.style.maxHeight = '100vh';
img.style.display = 'block'; img.style.display = 'block';
img.style.margin = 'auto'; img.style.margin = 'auto';
img.style.objectFit = 'contain'; img.style.objectFit = 'contain';
img.style.width = '100%'; img.style.width = '100vw';
div.appendChild(img); div.appendChild(img);
function image_close(event) { function image_close(event) {
document.body.removeChild(div); document.body.removeChild(div);
@@ -537,7 +537,7 @@ class TfMessageElement extends LitElement {
</style> </style>
<div <div
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top" class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%" style="overflow-wrap: anywhere; display: block; max-width: 100%"
> >
${inner} ${inner}
</div> </div>
@@ -789,60 +789,45 @@ class TfMessageElement extends LitElement {
</div> </div>
`); `);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return this.render_frame(html` switch (this.format) {
<div class="w3-bar"> case 'message':
<div class="w3-bar-item"> default:
<tf-user id=${this.message.author} .users=${this.users}></tf-user> return this.render_frame(html`
is <div class="w3-bar">
${content.blocking === true <div class="w3-bar-item">
? 'blocking' <tf-user
: content.blocking === false id=${this.message.author}
? 'no longer blocking' .users=${this.users}
: content.following === true ></tf-user>
? 'following' is
: content.following === false ${content.blocking === true
? 'no longer following' ? 'blocking'
: '?'} : content.blocking === false
<tf-user ? 'no longer blocking'
id=${this.message.content.contact} : content.following === true
.users=${this.users} ? 'following'
></tf-user> : content.following === false
</div> ? 'no longer following'
<div class="w3-bar-item w3-right"> : '?'}
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}> <tf-user
% id=${this.message.content.contact}
</button> .users=${this.users}
<div ></tf-user>
class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1" </div>
style="right: 48px" ${this.render_menu()} ${this.render_votes()}
> ${this.render_actions()}
<a
target="_top"
class="w3-button w3-bar-item"
href=${'#' + encodeURIComponent(this.message?.id)}
>View Message</a
>
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.copy_id}
>
Copy ID
</button>
${this.drafts[this.message?.id] === undefined
? html`
<button
class="w3-button w3-bar-item"
@click=${this.show_reply}
>
↩️ Reply
</button>
`
: undefined}
</div> </div>
`);
break;
case 'raw':
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
</div> </div>
${this.render_votes()} ${this.render_actions()} `);
</div> break;
`); }
} else if (content.type == 'post') { } else if (content.type == 'post') {
let self = this; let self = this;
let body; let body;

View File

@@ -14,6 +14,7 @@ class TfProfileElement extends LitElement {
sequence: {type: Number}, sequence: {type: Number},
following: {type: Boolean}, following: {type: Boolean},
blocking: {type: Boolean}, blocking: {type: Boolean},
show_followed: {type: Boolean},
}; };
} }
@@ -178,12 +179,12 @@ class TfProfileElement extends LitElement {
div.style.display = 'grid'; div.style.display = 'grid';
let img = document.createElement('img'); let img = document.createElement('img');
img.src = link; img.src = link;
img.style.maxWidth = '100%'; img.style.maxWidth = '100vw';
img.style.maxHeight = '100%'; img.style.maxHeight = '100vh';
img.style.display = 'block'; img.style.display = 'block';
img.style.margin = 'auto'; img.style.margin = 'auto';
img.style.objectFit = 'contain'; img.style.objectFit = 'contain';
img.style.width = '100%'; img.style.width = '100vw';
div.appendChild(img); div.appendChild(img);
function image_close(event) { function image_close(event) {
document.body.removeChild(div); document.body.removeChild(div);
@@ -202,11 +203,7 @@ class TfProfileElement extends LitElement {
toggle_account_list(event) { toggle_account_list(event) {
let content = event.srcElement.nextElementSibling; let content = event.srcElement.nextElementSibling;
if (content.classList.toggle('w3-hide')) { this.show_followed = !this.show_followed;
event.srcElement.innerText = 'Show Followed Accounts';
} else {
event.srcElement.innerText = 'Hide Followed Accounts';
}
} }
async load_follows() { async load_follows() {
@@ -214,12 +211,13 @@ class TfProfileElement extends LitElement {
return html` return html`
<div class="w3-container"> <div class="w3-container">
<button <button
class="w3-button w3-block w3-theme-d1" class="w3-button w3-block w3-theme-d1 followed_accounts"
@click=${this.toggle_account_list} @click=${this.toggle_account_list}
> >
Show Followed Accounts ${this.show_followed ? 'Hide' : 'Show'} Followed Accounts
(${Object.keys(accounts).length})
</button> </button>
<div class="w3-hide w3-card"> <div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}>
<ul class="w3-ul w3-theme-d4 w3-border-theme"> <ul class="w3-ul w3-theme-d4 w3-border-theme">
${Object.keys(accounts).map( ${Object.keys(accounts).map(
(x) => html` (x) => html`
@@ -329,7 +327,7 @@ class TfProfileElement extends LitElement {
</div> </div>
<div style="display: flex; flex-direction: row; gap: 1em"> <div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile} ${edit_profile}
<div style="flex: 1 0 50%"> <div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${ ${
image image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>` ? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
@@ -351,6 +349,9 @@ class TfProfileElement extends LitElement {
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)} ${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
<footer class="w3-container"> <footer class="w3-container">
<p> <p>
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
Open Private Chat
</a>
${edit} ${edit}
${follow} ${follow}
${block} ${block}

View File

@@ -43,6 +43,8 @@ const tf = css`
border-left: 4px solid #fff; border-left: 4px solid #fff;
padding: 8px; padding: 8px;
padding-left: 12px; padding-left: 12px;
margin-left: 0;
margin-right: 0;
} }
`; `;

View File

@@ -18,6 +18,7 @@ class TfTabNewsFeedElement extends LitElement {
time_range: {type: Array}, time_range: {type: Array},
time_loading: {type: Array}, time_loading: {type: Array},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
}; };
} }
@@ -106,8 +107,15 @@ class TfTabNewsFeedElement extends LitElement {
} }
async fetch_messages(start_time, end_time) { async fetch_messages(start_time, end_time) {
this.dispatchEvent(
new CustomEvent('loadmessages', {
bubbles: true,
composed: true,
})
);
this.time_loading = [start_time, end_time]; this.time_loading = [start_time, end_time];
let result; let result;
const k_max_results = 64;
if (this.hash == '#@') { if (this.hash == '#@') {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
@@ -118,7 +126,7 @@ class TfTabNewsFeedElement extends LitElement {
WHERE WHERE
messages.author != ?1 AND messages.author != ?1 AND
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4 (?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
ORDER BY timestamp DESC limit 20) ORDER BY timestamp DESC limit ?5)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref JOIN messages_refs ON mentions.id = messages_refs.ref
@@ -131,6 +139,7 @@ class TfTabNewsFeedElement extends LitElement {
JSON.stringify(this.following), JSON.stringify(this.following),
start_time, start_time,
end_time, end_time,
k_max_results,
] ]
); );
} else if (this.hash.startsWith('#@')) { } else if (this.hash.startsWith('#@')) {
@@ -140,7 +149,7 @@ class TfTabNewsFeedElement extends LitElement {
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3 WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence DESC LIMIT 20 ORDER BY sequence DESC LIMIT ?4
) )
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected FROM selected
@@ -149,7 +158,7 @@ class TfTabNewsFeedElement extends LitElement {
UNION UNION
SELECT TRUE AS is_primary, * FROM selected SELECT TRUE AS is_primary, * FROM selected
`, `,
[this.hash.substring(1), start_time, end_time] [this.hash.substring(1), start_time, end_time, k_max_results]
); );
} else if (this.hash.startsWith('#%')) { } else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
@@ -184,13 +193,14 @@ class TfTabNewsFeedElement extends LitElement {
) )
SELECT TRUE AS is_primary, all_news.* FROM all_news SELECT TRUE AS is_primary, all_news.* FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3 WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20 ORDER BY all_news.timestamp DESC LIMIT ?5
`, `,
[ [
JSON.stringify(this.following), JSON.stringify(this.following),
start_time, start_time,
end_time, end_time,
this.hash.substring(2), this.hash.substring(2),
k_max_results,
] ]
); );
let t1 = new Date(); let t1 = new Date();
@@ -199,7 +209,9 @@ class TfTabNewsFeedElement extends LitElement {
console.log( console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}` `load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
); );
} else if (this.hash == '#🔐') { } else if (this.hash.startsWith('#🔐')) {
let ids =
this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
@@ -208,9 +220,18 @@ class TfTabNewsFeedElement extends LitElement {
WHERE WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND (?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY messages.rowid DESC LIMIT 20 ORDER BY messages.rowid DESC LIMIT ?4
`, `,
[JSON.stringify(this.private_messages), start_time, end_time] [
JSON.stringify(
this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
(x) => x.id
) ?? []
),
start_time,
end_time,
k_max_results,
]
); );
result = (await this.decrypt(result)).filter((x) => x.decrypted); result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else if (this.hash == '#👍') { } else if (this.hash == '#👍') {
@@ -222,14 +243,14 @@ class TfTabNewsFeedElement extends LitElement {
WHERE WHERE
messages.content ->> 'type' = 'vote' AND messages.content ->> 'type' = 'vote' AND
(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3 (?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3
ORDER BY timestamp DESC limit 20) ORDER BY timestamp DESC limit ?4)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM votes FROM votes
JOIN messages ON messages.id = votes.content ->> '$.vote.link' JOIN messages ON messages.id = votes.content ->> '$.vote.link'
UNION UNION
SELECT TRUE AS is_primary, * FROM votes SELECT TRUE AS is_primary, * FROM votes
`, `,
[JSON.stringify(this.following), start_time, end_time] [JSON.stringify(this.following), start_time, end_time, k_max_results]
); );
} else { } else {
let t0 = new Date(); let t0 = new Date();
@@ -240,9 +261,9 @@ class TfTabNewsFeedElement extends LitElement {
JOIN json_each(?) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
messages.content ->> 'type' != 'vote' messages.content ->> 'type' != 'vote'
ORDER BY timestamp DESC LIMIT 20 ORDER BY timestamp DESC LIMIT ?4
`, `,
[JSON.stringify(this.following), start_time, end_time] [JSON.stringify(this.following), start_time, end_time, k_max_results]
); );
let t1 = new Date(); let t1 = new Date();
result = await this._fetch_related_messages(initial_messages); result = await this._fetch_related_messages(initial_messages);
@@ -365,12 +386,16 @@ class TfTabNewsFeedElement extends LitElement {
let self = this; let self = this;
this.loading++; this.loading++;
let messages = []; let messages = [];
let original_hash = this.hash;
try { try {
if (this._messages_hash !== this.hash) { if (this._messages_hash !== this.hash) {
this.messages = []; this.messages = [];
this._messages_hash = this.hash; this._messages_hash = this.hash;
} }
this._messages_following = this.following; this._messages_following = JSON.stringify(this.following);
this._private_messages =
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages);
let now = new Date().valueOf(); let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000; let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time; this.start_time = start_time;
@@ -383,7 +408,9 @@ class TfTabNewsFeedElement extends LitElement {
} finally { } finally {
this.loading--; this.loading--;
} }
this.messages = this.merge_messages(this.messages, messages); if (this.hash == original_hash) {
this.messages = this.merge_messages(this.messages, messages);
}
this.time_loading = undefined; this.time_loading = undefined;
console.log( console.log(
`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s` `loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
@@ -409,12 +436,42 @@ class TfTabNewsFeedElement extends LitElement {
} }
} }
close_private_chat() {
this.mark_all_read();
this.dispatchEvent(
new CustomEvent('closeprivatechat', {
bubbles: true,
composed: true,
detail: {
key: JSON.stringify(
this.hash == '#🔐'
? []
: this.hash.substring('#🔐'.length).split(',')
),
},
})
);
tfrpc.rpc.setHash('#');
}
render_close_chat_button() {
if (this.hash.startsWith('#🔐')) {
return html`
<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}>
Close Chat
</button>
`;
}
}
render() { render() {
if ( if (
!this.messages || !this.messages ||
this._messages_hash !== this.hash || this._messages_hash !== this.hash ||
JSON.stringify(this._messages_following) !== this._messages_following !== JSON.stringify(this.following) ||
JSON.stringify(this.following) this._private_messages !==
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages)
) { ) {
console.log( console.log(
`loading messages for ${this.whoami} (following ${this.following.length})` `loading messages for ${this.whoami} (following ${this.following.length})`
@@ -474,6 +531,7 @@ class TfTabNewsFeedElement extends LitElement {
Mark All Read Mark All Read
</button>` </button>`
: undefined} : undefined}
${this.render_close_chat_button()}
<tf-news <tf-news
id="news" id="news"
whoami=${this.whoami} whoami=${this.whoami}

View File

@@ -24,6 +24,7 @@ class TfTabNewsElement extends LitElement {
channels_latest: {type: Object}, channels_latest: {type: Object},
connections: {type: Array}, connections: {type: Array},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
recent_reactions: {type: Array}, recent_reactions: {type: Array},
peer_exchange: {type: Boolean}, peer_exchange: {type: Boolean},
is_administrator: {type: Boolean}, is_administrator: {type: Boolean},
@@ -115,6 +116,19 @@ class TfTabNewsElement extends LitElement {
) { ) {
return '✉️ '; return '✉️ ';
} }
} else if (channel?.startsWith('🔐')) {
let key = JSON.stringify(channel.substring('🔐'.length).split(','));
if (this.grouped_private_messages?.[key]) {
let grouped_latest = Math.max(
...this.grouped_private_messages?.[key]?.map((x) => x.rowid)
);
if (
this.channels_unread[channel] === undefined ||
grouped_latest > this.channels_unread[channel]
) {
return '✉️ ';
}
}
} else if ( } else if (
this.channels_latest[channel] && this.channels_latest[channel] &&
this.channels_latest[channel] > 0 && this.channels_latest[channel] > 0 &&
@@ -156,11 +170,8 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
} }
compare_follows() { compare_follows(a, b) {
const now = new Date().valueOf(); return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0;
return function (a, b) {
return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
};
} }
suggested_follows() { suggested_follows() {
@@ -169,9 +180,11 @@ class TfTabNewsElement extends LitElement {
** pinned at the top. ** pinned at the top.
*/ */
let self = this; let self = this;
let now = new Date().valueOf();
return Object.entries(this.users) return Object.entries(this.users)
.filter((x) => x[1].ts < now)
.filter((x) => x[1].follow_depth > 1) .filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows()) .sort(self.compare_follows)
.slice(0, 8) .slice(0, 8)
.map((x) => x[0]); .map((x) => x[0]);
} }
@@ -181,6 +194,10 @@ class TfTabNewsElement extends LitElement {
await this.check_peer_exchange(); await this.check_peer_exchange();
} }
is_loading() {
return this.shadowRoot?.getElementById('news')?.loading;
}
render_sidebar() { render_sidebar() {
return html` return html`
<div <div
@@ -203,7 +220,7 @@ class TfTabNewsElement extends LitElement {
new Event('refresh', {bubbles: true, composed: true}) new Event('refresh', {bubbles: true, composed: true})
)} )}
> >
<span style="width: 1.5em; height: 1.5em; padding: 8px">↻</span> <span style="display: inline-block; width: 1.8em">↻</span>
Sync now Sync now
</button> </button>
<button <button
@@ -216,7 +233,10 @@ class TfTabNewsElement extends LitElement {
}) })
)} )}
> >
${this.stay_connected ? '🔗 Online mode' : '⛓️‍💥 Passive mode'} <span style="display: inline-block; width: 1.8em"
>${this.stay_connected ? '🔗' : '⛓️‍💥'}</span
>
${this.stay_connected ? 'Online mode' : 'Passive mode'}
</button> </button>
` `
: undefined} : undefined}
@@ -251,12 +271,29 @@ class TfTabNewsElement extends LitElement {
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined} style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a >${this.unread_status('👍')}👍votes</a
> >
<a ${Object.keys(this?.grouped_private_messages ?? [])
href="#🔐" ?.sort()
class="w3-bar-item w3-button" ?.map(
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined} (key) => html`
>${this.unread_status('🔐')}🔐private</a <a
> href=${'#🔐' + JSON.parse(key).join(',')}
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' + JSON.parse(key).join(',')
? 'font-weight: bold'
: undefined}
>${this.unread_status('🔐' + JSON.parse(key).join(','))}
${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
(id) => html`
<tf-user
id=${id}
nolink="true"
.users=${this.users}
></tf-user>
`
)}</a
>
`
)}
${Object.keys(this.drafts) ${Object.keys(this.drafts)
.sort() .sort()
.map( .map(
@@ -374,7 +411,7 @@ class TfTabNewsElement extends LitElement {
return cache(html` return cache(html`
${this.render_sidebar()} ${this.render_sidebar()}
<div <div
style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto; contain: layout" style="margin-left: 2in; padding: 0px; top: 0; height: 100vh; max-height: 100%; overflow: auto; contain: layout"
id="main" id="main"
class="w3-main" class="w3-main"
> >
@@ -412,6 +449,9 @@ class TfTabNewsElement extends LitElement {
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-draft=${this.draft} @tf-draft=${this.draft}
.channel=${this.channel()} .channel=${this.channel()}
.recipients=${this.hash.startsWith('#🔐')
? this.hash.substring('#🔐'.length).split(',')
: undefined}
></tf-compose> ></tf-compose>
</div> </div>
${profile} ${profile}
@@ -428,6 +468,7 @@ class TfTabNewsElement extends LitElement {
.channels_unread=${this.channels_unread} .channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest} .channels_latest=${this.channels_latest}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.grouped_private_messages=${this.grouped_private_messages}
.recent_reactions=${this.recent_reactions} .recent_reactions=${this.recent_reactions}
></tf-tab-news-feed> ></tf-tab-news-feed>
</div> </div>

View File

@@ -9,6 +9,7 @@ class TfUserElement extends LitElement {
fallback_name: {type: String}, fallback_name: {type: String},
icon_only: {type: Boolean}, icon_only: {type: Boolean},
users: {type: Object}, users: {type: Object},
nolink: {type: Boolean},
}; };
} }
@@ -37,7 +38,9 @@ class TfUserElement extends LitElement {
let name_string = name ?? this.fallback_name ?? this.id; let name_string = name ?? this.fallback_name ?? this.id;
name = this.icon_only name = this.icon_only
? undefined ? undefined
: html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`; : !this.nolink
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
: html`<span>${name_string}</span>`;
if (user) { if (user) {
let image_link = user.image; let image_link = user.image;
@@ -56,7 +59,8 @@ class TfUserElement extends LitElement {
} }
} }
return html` <div return html` <div
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis" style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
(this.nolink ? '' : '; font-weight: bold')}
> >
${image} ${name} ${image} ${name}
</div>`; </div>`;

View File

@@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "👋", "emoji": "👋",
"previous": "&3puDxDNnf6C+YXpFysYLgxFMAy54/AO9V7Xpja6qO/k=.sha256" "previous": "&5NkMRSgcMqCYF3xcLOBmaytkoxfV9zx4br7JladKPTs=.sha256"
} }

View File

@@ -1,5 +0,0 @@
async function main() {
await app.setDocument(utf8Decode(getFile('index.html')));
}
main();

1
apps/welcome/gitea.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -47,8 +47,10 @@
<a <a
class="w3-button w3-black w3-padding-large" class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/cory/tildefriends" href="https://dev.tildefriends.net/cory/tildefriends"
><i class="fa fa-mug-hot"></i> Development</a
> >
<img src="gitea.svg" style="height: 1em; margin: 0" />
Development
</a>
<a <a
class="w3-button w3-black w3-padding-large" class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/" href="https://docs.tildefriends.net/"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,23 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js'; import * as core from './core.js';
let gSessionIndex = 0; export {App};
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** App constructor.
** @return An app instance.
*/
function App() { function App() {
this._send_queue = []; this._send_queue = [];
this.calls = {}; this.calls = {};
@@ -9,6 +25,12 @@ function App() {
return this; return this;
} }
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
App.prototype.makeFunction = function (api) { App.prototype.makeFunction = function (api) {
let self = this; let self = this;
let result = function () { let result = function () {
@@ -32,6 +54,10 @@ App.prototype.makeFunction = function (api) {
return result; return result;
}; };
/**
** Send a message to the app.
** @param message The message to send.
*/
App.prototype.send = function (message) { App.prototype.send = function (message) {
if (this._send_queue) { if (this._send_queue) {
if (this._on_output) { if (this._on_output) {
@@ -46,6 +72,11 @@ App.prototype.send = function (message) {
} }
}; };
/**
** App socket handler.
** @param request The HTTP request of the WebSocket connection.
** @param response The HTTP response.
*/
exports.app_socket = async function socket(request, response) { exports.app_socket = async function socket(request, response) {
let process; let process;
let options = {}; let options = {};
@@ -133,7 +164,7 @@ exports.app_socket = async function socket(request, response) {
options.packageOwner = packageOwner; options.packageOwner = packageOwner;
options.packageName = packageName; options.packageName = packageName;
options.url = message.url; options.url = message.url;
let sessionId = 'session_' + (gSessionIndex++).toString(); let sessionId = 'session_' + (g_session_index++).toString();
if (blobId) { if (blobId) {
if (message.edit_only) { if (message.edit_only) {
response.send( response.send(
@@ -218,4 +249,4 @@ exports.app_socket = async function socket(request, response) {
response.upgrade(100, {}); response.upgrade(100, {});
}; };
export {App}; /** @} */

View File

@@ -1,3 +1,11 @@
/**
* \file
* \defgroup tfclient Tilde Friends Client JS
* Tilde Friends client-side browser JavaScript.
* @{
*/
/** \cond */
import {LitElement, html, css, svg} from '/lit/lit-all.min.js'; import {LitElement, html, css, svg} from '/lit/lit-all.min.js';
let cm6; let cm6;
@@ -13,8 +21,9 @@ let gUnloading;
let kErrorColor = '#dc322f'; let kErrorColor = '#dc322f';
let kDisconnectColor = '#f00'; let kDisconnectColor = '#f00';
let kStatusColor = '#fff'; let kStatusColor = '#fff';
/** \endcond */
// Functions that server-side app code can call through the app object. /** Functions that server-side app code can call through the app object. */
const k_api = { const k_api = {
setDocument: {args: ['content'], func: api_setDocument}, setDocument: {args: ['content'], func: api_setDocument},
postMessage: {args: ['message'], func: api_postMessage}, postMessage: {args: ['message'], func: api_postMessage},
@@ -26,29 +35,14 @@ const k_api = {
setHash: {args: ['hash'], func: api_setHash}, setHash: {args: ['hash'], func: api_setHash},
}; };
// TODO(tasiaiso): this is only used once, move it down ?
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
/** /**
* Class that represents the top bar * Class that represents the top bar
*/ */
class TfNavigationElement extends LitElement { class TfNavigationElement extends LitElement {
/**
* Get Lit Html properties.
* @return The properties.
*/
static get properties() { static get properties() {
return { return {
credentials: {type: Object}, credentials: {type: Object},
@@ -64,6 +58,9 @@ class TfNavigationElement extends LitElement {
}; };
} }
/**
* Create a TfNavigationElement instance.
*/
constructor() { constructor() {
super(); super();
this.permissions = {}; this.permissions = {};
@@ -75,8 +72,8 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Toggle editor visibility.
* @param {*} event * @param event The HTML event.
*/ */
toggle_edit(event) { toggle_edit(event) {
event.preventDefault(); event.preventDefault();
@@ -88,18 +85,18 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Remove a stored permission.
* @param {*} key * @param key The permission to reset.
*/ */
reset_permission(key) { reset_permission(key) {
send({action: 'resetPermission', permission: key}); send({action: 'resetPermission', permission: key});
} }
/** /**
* TODOC * Get or create a spark line.
* @param {*} key * @param key The spark line identifier.
* @param {*} options * @param options Spark line options.
* @returns * @return A spark line HTML element.
*/ */
get_spark_line(key, options) { get_spark_line(key, options) {
if (!this.spark_lines[key]) { if (!this.spark_lines[key]) {
@@ -118,29 +115,49 @@ class TfNavigationElement extends LitElement {
return this.spark_lines[key]; return this.spark_lines[key];
} }
/**
* Set the active SSB identity for the current application.
* @param id The identity.
*/
set_active_identity(id) { set_active_identity(id) {
send({action: 'setActiveIdentity', identity: id}); send({action: 'setActiveIdentity', identity: id});
this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show'); this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
} }
create_identity(event) { /**
* Create a new SSB identity.
*/
create_identity() {
if (confirm('Are you sure you want to create a new identity?')) { if (confirm('Are you sure you want to create a new identity?')) {
send({action: 'createIdentity'}); send({action: 'createIdentity'});
} }
} }
/**
* Toggle visibility of the ID dropdown.
*/
toggle_id_dropdown() { toggle_id_dropdown() {
this.renderRoot.getElementById('id_dropdown').classList.toggle('w3-show'); this.renderRoot.getElementById('id_dropdown').classList.toggle('w3-show');
} }
/**
* Edit the current identity's SSB profile.
*/
edit_profile() { edit_profile() {
window.location.href = '/~core/ssb/#' + this.identity; window.location.href = '/~core/ssb/#' + this.identity;
} }
/**
* Sign out of the current Tilde Friends user.
*/
logout() { logout() {
window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`; window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
} }
/**
* Render the identity dropdown.
* @return Lit HTML.
*/
render_identity() { render_identity() {
let self = this; let self = this;
@@ -245,8 +262,8 @@ class TfNavigationElement extends LitElement {
} }
/** /**
* TODOC * Render the permissions popup.
* @returns * @return Lit HTML.
*/ */
render_permissions() { render_permissions() {
if (this.show_permissions) { if (this.show_permissions) {
@@ -287,16 +304,36 @@ class TfNavigationElement extends LitElement {
} }
} }
/**
* Clear the current error.
*/
clear_error() { clear_error() {
this.status = {}; this.status = {};
} }
/** /**
* TODOC * Render the navigation bar.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
const k_global_style = css`
a:link {
color: #268bd2;
}
a:visited {
color: #6c71c4;
}
a:hover {
color: #859900;
}
a:active {
color: #2aa198;
}
`;
return html` return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" /> <link type="text/css" rel="stylesheet" href="/static/w3.css" />
<style> <style>
@@ -401,12 +438,19 @@ class TfNavigationElement extends LitElement {
} }
} }
/**
* Create a tf-navigation element.
*/
customElements.define('tf-navigation', TfNavigationElement); customElements.define('tf-navigation', TfNavigationElement);
/** /**
* TODOC * A file in the files sidebar.
*/ */
class TfFilesElement extends LitElement { class TfFilesElement extends LitElement {
/**
* LitElement properties.
* @return The properties.
*/
static get properties() { static get properties() {
return { return {
current: {type: String}, current: {type: String},
@@ -416,6 +460,9 @@ class TfFilesElement extends LitElement {
}; };
} }
/**
* Create a TfFilesElement instance.
*/
constructor() { constructor() {
super(); super();
this.files = {}; this.files = {};
@@ -423,8 +470,8 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Select a clicked file.
* @param {*} file * @param file The file.
*/ */
file_click(file) { file_click(file) {
this.dispatchEvent( this.dispatchEvent(
@@ -439,9 +486,9 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Render a single file in the file list.
* @param {*} file * @param file The file.
* @returns * @return Lit HTML.
*/ */
render_file(file) { render_file(file) {
let classes = ['file']; let classes = ['file'];
@@ -463,8 +510,8 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Create a file entry for a dropped file.
* @param {*} event * @param event The event.
*/ */
async drop(event) { async drop(event) {
event.preventDefault(); event.preventDefault();
@@ -489,8 +536,8 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Called when a file starts being dragged over the file.
* @param {*} event * @param event The event.
*/ */
drag_enter(event) { drag_enter(event) {
this.dropping++; this.dropping++;
@@ -499,8 +546,8 @@ class TfFilesElement extends LitElement {
} }
/** /**
* TODOC * Called when a file stops being dragged over the file.
* @param {*} event * @param event The event.
*/ */
drag_leave(event) { drag_leave(event) {
this.dropping--; this.dropping--;
@@ -509,13 +556,17 @@ class TfFilesElement extends LitElement {
} }
} }
/**
* Called when a file is being dragged over the file.
* @param event The event.
*/
drag_over(event) { drag_over(event) {
event.preventDefault(); event.preventDefault();
} }
/** /**
* TODOC * Render the file.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
@@ -562,9 +613,13 @@ class TfFilesElement extends LitElement {
customElements.define('tf-files', TfFilesElement); customElements.define('tf-files', TfFilesElement);
/** /**
* TODOC * The files pane element.
*/ */
class TfFilesPaneElement extends LitElement { class TfFilesPaneElement extends LitElement {
/**
* Get Lit Html properties.
* @return The properties.
*/
static get properties() { static get properties() {
return { return {
expanded: {type: Boolean}, expanded: {type: Boolean},
@@ -573,6 +628,9 @@ class TfFilesPaneElement extends LitElement {
}; };
} }
/**
* Create a TfFilesPaneElement instance.
*/
constructor() { constructor() {
super(); super();
this.expanded = window.localStorage.getItem('files') != '0'; this.expanded = window.localStorage.getItem('files') != '0';
@@ -580,8 +638,8 @@ class TfFilesPaneElement extends LitElement {
} }
/** /**
* TODOC * Set whether the files pane is expanded.
* @param {*} expanded * @param expanded Whether the files pane is expanded.
*/ */
set_expanded(expanded) { set_expanded(expanded) {
this.expanded = expanded; this.expanded = expanded;
@@ -589,8 +647,8 @@ class TfFilesPaneElement extends LitElement {
} }
/** /**
* TODOC * Render the files pane element.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let self = this; let self = this;
@@ -649,7 +707,7 @@ class TfFilesPaneElement extends LitElement {
customElements.define('tf-files-pane', TfFilesPaneElement); customElements.define('tf-files-pane', TfFilesPaneElement);
/** /**
* TODOC * A tiny graph.
*/ */
class TfSparkLineElement extends LitElement { class TfSparkLineElement extends LitElement {
static get properties() { static get properties() {
@@ -669,9 +727,9 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Add a data point to the graph.
* @param {*} key * @param key The line to which the point applies.
* @param {*} value * @param value The numeric value of the data point.
*/ */
append(key, value) { append(key, value) {
let line = null; let line = null;
@@ -698,9 +756,9 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Render a single series line.
* @param {*} line * @param line The line data.
* @returns * @return Lit HTML.
*/ */
render_line(line) { render_line(line) {
if (line?.values?.length >= 2) { if (line?.values?.length >= 2) {
@@ -716,8 +774,8 @@ class TfSparkLineElement extends LitElement {
} }
/** /**
* TODOC * Render the graph.
* @returns * @return Lit HTML.
*/ */
render() { render() {
let max = let max =
@@ -744,7 +802,9 @@ class TfSparkLineElement extends LitElement {
customElements.define('tf-sparkline', TfSparkLineElement); customElements.define('tf-sparkline', TfSparkLineElement);
// TODOC /**
* A keyboard key is pressed down.
*/
window.addEventListener('keydown', function (event) { window.addEventListener('keydown', function (event) {
if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) { if (event.keyCode == 83 && (event.altKey || event.ctrlKey)) {
if (editing()) { if (editing()) {
@@ -760,10 +820,9 @@ window.addEventListener('keydown', function (event) {
}); });
/** /**
* TODOC * Make sure a set of dependencies are loaded
* @param {*} nodes * @param nodes An array of descriptions of dependencies to load.
* @param {*} callback * @param callback Called when all dependencies are loaded.
* @returns
*/ */
function ensureLoaded(nodes, callback) { function ensureLoaded(nodes, callback) {
if (!nodes.length) { if (!nodes.length) {
@@ -806,24 +865,23 @@ function ensureLoaded(nodes, callback) {
} }
/** /**
* TODOC * Check whether the editior is currently visible.
* @returns * @return true if the editor is visible.
*/ */
function editing() { function editing() {
return document.getElementById('editPane').style.display != 'none'; return document.getElementById('editPane').style.display != 'none';
} }
/** /**
* TODOC * Check whether only the editor is visible and the app is hidden.
* @returns * @return true if the editor is visible and the app is not.
*/ */
function is_edit_only() { function is_edit_only() {
return window.location.search == '?editonly=1' || window.innerWidth < 1024; return window.location.search == '?editonly=1' || window.innerWidth < 1024;
} }
/** /**
* TODOC * Show the editor.
* @returns
*/ */
async function edit() { async function edit() {
if (editing()) { if (editing()) {
@@ -850,30 +908,17 @@ async function edit() {
} }
/** /**
* TODOC * Open a performance trace.
*/ */
function trace() { function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`); window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
} }
/** /**
* TODOC * Load a single file.
* @param {*} name * @param name The name by which the file is known.
* @returns * @param id The file's ID.
*/ * @return A promise resolved with the file's contents.
function guessMode(name) {
return name.endsWith('.js')
? 'javascript'
: name.endsWith('.html')
? 'htmlmixed'
: null;
}
/**
* TODOC
* @param {*} name
* @param {*} id
* @returns
*/ */
function loadFile(name, id) { function loadFile(name, id) {
return fetch('/' + id + '/view') return fetch('/' + id + '/view')
@@ -899,9 +944,9 @@ function loadFile(name, id) {
} }
/** /**
* TODOC * Load files for the app.
* @param {*} path * @param path The app path to load.
* @returns * @return A promise resolved when the app is laoded.
*/ */
async function load(path) { async function load(path) {
let response = await fetch((path || url()) + 'view'); let response = await fetch((path || url()) + 'view');
@@ -941,7 +986,7 @@ async function load(path) {
} }
/** /**
* TODOC * Hide the editor.
*/ */
function closeEditor() { function closeEditor() {
window.localStorage.setItem('editing', '0'); window.localStorage.setItem('editing', '0');
@@ -950,17 +995,9 @@ function closeEditor() {
} }
/** /**
* TODOC * Save the app.
* @returns * @param save_to An optional path to which to save the app.
*/ * @return A promise resoled when the app is saved.
function explodePath() {
return /^\/~([^\/]+)\/([^\/]+)(.*)/.exec(window.location.pathname);
}
/**
* TODOC
* @param {*} save_to
* @returns
*/ */
function save(save_to) { function save(save_to) {
document.getElementById('save').disabled = true; document.getElementById('save').disabled = true;
@@ -1050,6 +1087,8 @@ function save(save_to) {
if (save_path != window.location.pathname) { if (save_path != window.location.pathname) {
alert('Saved to ' + save_path + '.'); alert('Saved to ' + save_path + '.');
} else if (!gFiles['app.js']) {
window.location.reload();
} else { } else {
reconnect(save_path); reconnect(save_path);
} }
@@ -1068,7 +1107,7 @@ function save(save_to) {
} }
/** /**
* TODOC * Prompt to set the app icon.
*/ */
function changeIcon() { function changeIcon() {
let value = prompt('Enter a new app icon emoji:'); let value = prompt('Enter a new app icon emoji:');
@@ -1079,7 +1118,7 @@ function changeIcon() {
} }
/** /**
* TODOC * Prompt to delete the current app.
*/ */
function deleteApp() { function deleteApp() {
let name = document.getElementById('name'); let name = document.getElementById('name');
@@ -1100,8 +1139,8 @@ function deleteApp() {
} }
/** /**
* TODOC * Get the current app URL.
* @returns * @return The app URL.
*/ */
function url() { function url() {
let hash = window.location.href.indexOf('#'); let hash = window.location.href.indexOf('#');
@@ -1119,16 +1158,16 @@ function url() {
} }
/** /**
* TODOC * Get the window hash without the lone '#' if it is empty.
* @returns * @return The hash.
*/ */
function hash() { function hash() {
return window.location.hash != '#' ? window.location.hash : ''; return window.location.hash != '#' ? window.location.hash : '';
} }
/** /**
* TODOC * Set the iframe document contents.
* @param {*} content * @param content The contents.
*/ */
function api_setDocument(content) { function api_setDocument(content) {
let iframe = document.getElementById('document'); let iframe = document.getElementById('document');
@@ -1136,8 +1175,8 @@ function api_setDocument(content) {
} }
/** /**
* TODOC * Send a message to the sandboxed iframe.
* @param {*} message * @param message The message.
*/ */
function api_postMessage(message) { function api_postMessage(message) {
let iframe = document.getElementById('document'); let iframe = document.getElementById('document');
@@ -1145,8 +1184,8 @@ function api_postMessage(message) {
} }
/** /**
* TODOC * Show an error.
* @param {*} error * @param error The error.
*/ */
function api_error(error) { function api_error(error) {
if (error) { if (error) {
@@ -1160,28 +1199,28 @@ function api_error(error) {
} }
/** /**
* TODOC et a value in local storage.
* @param {*} key * @param key The key.
* @param {*} value * @param value The value.
*/ */
function api_localStorageSet(key, value) { function api_localStorageSet(key, value) {
window.localStorage.setItem('app:' + key, value); window.localStorage.setItem('app:' + key, value);
} }
/** /**
* TODOC * Get a value from local storage.
* @param {*} key * @param key The key.
* @returns * @return The value.
*/ */
function api_localStorageGet(key) { function api_localStorageGet(key) {
return window.localStorage.getItem('app:' + key); return window.localStorage.getItem('app:' + key);
} }
/** /**
* TODOC * Request a permission
* @param {*} permission * @param permission The permission to request.
* @param {*} id * @param id The id requeesting the permission.
* @returns * @return A promise fulfilled if the permission was granted.
*/ */
function api_requestPermission(permission, id) { function api_requestPermission(permission, id) {
let outer = document.createElement('div'); let outer = document.createElement('div');
@@ -1250,23 +1289,23 @@ function api_requestPermission(permission, id) {
} }
/** /**
* TODOC * Log from the app to the console.
*/ */
function api_print() { function api_print() {
console.log('app>', ...arguments); console.log('app>', ...arguments);
} }
/** /**
* TODOC * Set the window's location hash.
* @param {*} hash * @param hash The new hash.
*/ */
function api_setHash(hash) { function api_setHash(hash) {
window.location.hash = hash; window.location.hash = hash;
} }
/** /**
* TODOC * Process an incoming WebSocket message.
* @param {*} message * @param message The message.
*/ */
function _receive_websocket_message(message) { function _receive_websocket_message(message) {
if (message && message.action == 'session') { if (message && message.action == 'session') {
@@ -1362,9 +1401,9 @@ function _receive_websocket_message(message) {
} }
/** /**
* TODOC * Set the status message.
* @param {*} message * @param message The message.
* @param {*} color * @param color The message's color.
*/ */
function setStatusMessage(message, color) { function setStatusMessage(message, color) {
document.getElementsByTagName('tf-navigation')[0].status = { document.getElementsByTagName('tf-navigation')[0].status = {
@@ -1375,8 +1414,8 @@ function setStatusMessage(message, color) {
} }
/** /**
* TODOC * Send a message to the app.
* @param {*} value * @param value The message.
*/ */
function send(value) { function send(value) {
try { try {
@@ -1389,57 +1428,14 @@ function send(value) {
} }
/** /**
* TODOC * Notify the app of the window hash changing.
* @param {*} sourceData
* @param {*} maxWidth
* @param {*} maxHeight
* @param {*} callback
*/
function fixImage(sourceData, maxWidth, maxHeight, callback) {
let result = sourceData;
let image = new Image();
image.crossOrigin = 'anonymous';
image.referrerPolicy = 'no-referrer';
image.onload = function () {
if (image.width > maxWidth || image.height > maxHeight) {
let downScale = Math.min(
maxWidth / image.width,
maxHeight / image.height
);
let canvas = document.createElement('canvas');
canvas.width = image.width * downScale;
canvas.height = image.height * downScale;
let context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
image.width = canvas.width;
image.height = canvas.height;
context.drawImage(image, 0, 0, image.width, image.height);
result = canvas.toDataURL();
}
callback(result);
};
image.src = sourceData;
}
/**
* TODOC
* @param {*} image
*/
function sendImage(image) {
fixImage(image, 320, 240, function (result) {
send({image: result});
});
}
/**
* TODOC
*/ */
function hashChange() { function hashChange() {
send({event: 'hashChange', hash: window.location.hash}); send({event: 'hashChange', hash: window.location.hash});
} }
/** /**
* TODOC * Make sure the app is connected on window focus, and notify the app.
*/ */
function focus() { function focus() {
if (gSocket && gSocket.readyState == gSocket.CLOSED) { if (gSocket && gSocket.readyState == gSocket.CLOSED) {
@@ -1450,7 +1446,7 @@ function focus() {
} }
/** /**
* TODOC * Notify the app of lost focus.
*/ */
function blur() { function blur() {
if (gSocket && gSocket.readyState == gSocket.OPEN) { if (gSocket && gSocket.readyState == gSocket.OPEN) {
@@ -1459,8 +1455,8 @@ function blur() {
} }
/** /**
* TODOC * Handle a message.
* @param {*} event * @param event The message.
*/ */
function message(event) { function message(event) {
if ( if (
@@ -1508,8 +1504,8 @@ function message(event) {
} }
/** /**
* TODOC * Reconnect the WebSocket.
* @param {*} path * @param path The path to which the WebSocket should be connected.
*/ */
function reconnect(path) { function reconnect(path) {
let oldSocket = gSocket; let oldSocket = gSocket;
@@ -1524,8 +1520,8 @@ function reconnect(path) {
} }
/** /**
* TODOC * Connect the WebSocket.
* @param {*} path * @param path The path to which to connect.
*/ */
function connectSocket(path) { function connectSocket(path) {
if (!gSocket || gSocket.readyState != gSocket.OPEN) { if (!gSocket || gSocket.readyState != gSocket.OPEN) {
@@ -1591,8 +1587,8 @@ function connectSocket(path) {
} }
/** /**
* TODOC * Open a file by name.
* @param {*} name * @param name The file to open.
*/ */
function openFile(name) { function openFile(name) {
let newDoc = let newDoc =
@@ -1617,7 +1613,7 @@ function openFile(name) {
} }
/** /**
* TODOC * Refresh the files list.
*/ */
function updateFiles() { function updateFiles() {
let files = document.getElementsByTagName('tf-files-pane')[0]; let files = document.getElementsByTagName('tf-files-pane')[0];
@@ -1639,8 +1635,8 @@ function updateFiles() {
} }
/** /**
* TODOC * Create a new file with the given name.
* @param {*} name * @param name The file's name.
*/ */
function makeNewFile(name) { function makeNewFile(name) {
gFiles[name] = { gFiles[name] = {
@@ -1650,7 +1646,7 @@ function makeNewFile(name) {
} }
/** /**
* TODOC * Prompt to create a new file.
*/ */
function newFile() { function newFile() {
let name = prompt('Name of new file:', 'file.js'); let name = prompt('Name of new file:', 'file.js');
@@ -1660,7 +1656,7 @@ function newFile() {
} }
/** /**
* TODOC * Prompt to remove a file.
*/ */
function removeFile() { function removeFile() {
if (confirm('Remove ' + gCurrentFile + '?')) { if (confirm('Remove ' + gCurrentFile + '?')) {
@@ -1670,7 +1666,7 @@ function removeFile() {
} }
/** /**
* TODOC * Export the app to a zip file, which is downloaded by the browser.
*/ */
async function appExport() { async function appExport() {
let JsZip = (await import('/static/jszip.min.js')).default; let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1701,10 +1697,10 @@ async function appExport() {
} }
/** /**
* TODOC * Save a file.
* @param {*} name * @param name The file to svae.
* @param {*} file * @param file The file contents.
* @returns * @return A promise resolved with the blob ID of the saved file.
*/ */
async function save_file_to_blob_id(name, file) { async function save_file_to_blob_id(name, file) {
console.log(`Saving ${name}.`); console.log(`Saving ${name}.`);
@@ -1728,7 +1724,7 @@ async function save_file_to_blob_id(name, file) {
} }
/** /**
* TODOC * Prompt to import an app from a zip file.
*/ */
async function appImport() { async function appImport() {
let JsZip = (await import('/static/jszip.min.js')).default; let JsZip = (await import('/static/jszip.min.js')).default;
@@ -1799,7 +1795,7 @@ async function appImport() {
} }
/** /**
* * Prettify the current source file.
*/ */
async function sourcePretty() { async function sourcePretty() {
let prettier = (await import('/prettier/standalone.mjs')).default; let prettier = (await import('/prettier/standalone.mjs')).default;
@@ -1827,6 +1823,9 @@ async function sourcePretty() {
} }
} }
/**
* Toggle visible whitespace.
*/
function toggleVisibleWhitespace() { function toggleVisibleWhitespace() {
let editor_style = document.getElementById('editor_style'); let editor_style = document.getElementById('editor_style');
/* /*
@@ -1852,7 +1851,9 @@ function toggleVisibleWhitespace() {
} }
} }
// TODOC /**
* Register event handlers and connect the WebSocket on load.
*/
window.addEventListener('load', function () { window.addEventListener('load', function () {
window.addEventListener('hashchange', hashChange); window.addEventListener('hashchange', hashChange);
window.addEventListener('focus', focus); window.addEventListener('focus', focus);
@@ -1900,3 +1901,5 @@ window.addEventListener('load', function () {
toggleVisibleWhitespace(); toggleVisibleWhitespace();
} }
}); });
/** @} */

View File

@@ -1,12 +1,30 @@
/**
* \file
* \defgroup tfcore Tilde Friends Core JS
* Tilde Friends process management, in JavaScript.
* @{
*/
/** \cond */
import * as app from './app.js'; import * as app from './app.js';
import * as http from './http.js'; import * as http from './http.js';
let gProcesses = {}; export {invoke, getProcessBlob};
let gStatsTimer = false; /** \endcond */
let g_handler_index = 0;
/** All running processes. */
let gProcesses = {};
/** Whether stats are currently being sent. */
let gStatsTimer = false;
/** Effectively a process ID. */
let g_handler_index = 0;
/** Time between pings, in milliseconds. */
const k_ping_interval = 60 * 1000; const k_ping_interval = 60 * 1000;
/**
* Print an error.
* @param error The error.
*/
function printError(error) { function printError(error) {
if (error.stackTrace) { if (error.stackTrace) {
print(error.fileName + ':' + error.lineNumber + ': ' + error.message); print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
@@ -19,6 +37,12 @@ function printError(error) {
} }
} }
/**
* Invoke a handler.
* @param handlers The handlers on which to invoke the callback.
* @param argv Arguments to pass to the handlers.
* @return A promise.
*/
function invoke(handlers, argv) { function invoke(handlers, argv) {
let promises = []; let promises = [];
if (handlers) { if (handlers) {
@@ -39,6 +63,12 @@ function invoke(handlers, argv) {
return Promise.all(promises); return Promise.all(promises);
} }
/**
* Broadcast a named event to all registered apps.
* @param eventName the name of the event.
* @param argv Arguments to pass to the handlers.
* @return A promise.
*/
function broadcastEvent(eventName, argv) { function broadcastEvent(eventName, argv) {
let promises = []; let promises = [];
for (let process of Object.values(gProcesses)) { for (let process of Object.values(gProcesses)) {
@@ -49,6 +79,11 @@ function broadcastEvent(eventName, argv) {
return Promise.all(promises); return Promise.all(promises);
} }
/**
* Send a message to all other instances of the same app.
* @param message The message.
* @return A promise.
*/
function broadcast(message) { function broadcast(message) {
let sender = this; let sender = this;
let promises = []; let promises = [];
@@ -65,6 +100,15 @@ function broadcast(message) {
return Promise.all(promises); return Promise.all(promises);
} }
/**
* Send a message to all instances of the same app running as the same user.
* @param user The user.
* @param packageOwner The owner of the app.
* @param packageName The name of the app.
* @param eventName The name of the event.
* @param argv The arguments to pass.
* @return A promise.
*/
function broadcastAppEventToUser( function broadcastAppEventToUser(
user, user,
packageOwner, packageOwner,
@@ -87,6 +131,11 @@ function broadcastAppEventToUser(
return Promise.all(promises); return Promise.all(promises);
} }
/**
* Get user context information for a call.
* @param caller The calling process.
* @param process The receiving process.
*/
function getUser(caller, process) { function getUser(caller, process) {
return { return {
key: process.key, key: process.key,
@@ -97,38 +146,26 @@ function getUser(caller, process) {
}; };
} }
async function getApps(user, process) { /**
if ( * Send a message.
process.credentials && * @param from The calling process.
process.credentials.session && * @param to The receiving process.
process.credentials.session.name * @param message The message.
) { * @return A promise.
if (user && user !== process.credentials.session.name && user !== 'core') { */
return {};
} else if (!user) {
user = process.credentials.session.name;
}
}
if (user) {
let db = new Database(user);
try {
let names = JSON.parse(await db.get('apps'));
let result = {};
for (let name of names) {
result[name] = await db.get('path:' + name);
}
return result;
} catch {}
}
return {};
}
function postMessageInternal(from, to, message) { function postMessageInternal(from, to, message) {
if (to.eventHandlers['message']) { if (to.eventHandlers['message']) {
return invoke(to.eventHandlers['message'], [getUser(from, from), message]); return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
} }
} }
/**
* Get or create a process for an app blob.
* @param blobId The blob identifier.
* @param key A unique key for the invocation.
* @param options Other options.
* @return The process.
*/
async function getProcessBlob(blobId, key, options) { async function getProcessBlob(blobId, key, options) {
let process = gProcesses[key]; let process = gProcesses[key];
if (!process && !(options && 'create' in options && !options.create)) { if (!process && !(options && 'create' in options && !options.create)) {
@@ -162,23 +199,6 @@ async function getProcessBlob(blobId, key, options) {
let imports = { let imports = {
core: { core: {
broadcast: broadcast.bind(process), broadcast: broadcast.bind(process),
register: function (eventName, handler) {
if (!process.eventHandlers[eventName]) {
process.eventHandlers[eventName] = [];
}
process.eventHandlers[eventName].push(handler);
},
unregister: function (eventName, handler) {
if (process.eventHandlers[eventName]) {
let index = process.eventHandlers[eventName].indexOf(handler);
if (index != -1) {
process.eventHandlers[eventName].splice(index, 1);
}
if (process.eventHandlers[eventName].length == 0) {
delete process.eventHandlers[eventName];
}
}
},
user: getUser(process, process), user: getUser(process, process),
users: async function () { users: async function () {
try { try {
@@ -220,7 +240,6 @@ async function getProcessBlob(blobId, key, options) {
let settings = await loadSettings(); let settings = await loadSettings();
return settings?.permissions?.[user] ?? []; return settings?.permissions?.[user] ?? [];
}, },
apps: (user) => getApps(user, process),
getSockets: getSockets, getSockets: getSockets,
permissionTest: async function (permission) { permissionTest: async function (permission) {
let user = process?.credentials?.session?.name; let user = process?.credentials?.session?.name;
@@ -667,6 +686,9 @@ async function getProcessBlob(blobId, key, options) {
return process; return process;
} }
/**
* SSB message added callback.
*/
ssb.addEventListener('message', function () { ssb.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]); broadcastEvent('onMessage', [...arguments]);
}); });
@@ -679,6 +701,10 @@ ssb.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []); broadcastEvent('onConnectionsChanged', []);
}); });
/**
* Load settings from the database.
* @return The settings as a key value pairs object.
*/
async function loadSettings() { async function loadSettings() {
let data = {}; let data = {};
try { try {
@@ -697,6 +723,9 @@ async function loadSettings() {
return data; return data;
} }
/**
* Send periodic stats to all clients.
*/
function sendStats() { function sendStats() {
let apps = Object.values(gProcesses) let apps = Object.values(gProcesses)
.filter((process) => process.app) .filter((process) => process.app)
@@ -712,6 +741,16 @@ function sendStats() {
} }
} }
/**
* Invoke an app's handler.js.
* @param response The response object.
* @param app_blob_id The app's blob identifier.
* @param path The request path.
* @param query The request query string.
* @param headers The request headers.
* @param package_owner The app's owner.
* @param package_name The app's name.
*/
exports.callAppHandler = async function callAppHandler( exports.callAppHandler = async function callAppHandler(
response, response,
app_blob_id, app_blob_id,
@@ -777,4 +816,4 @@ exports.callAppHandler = async function callAppHandler(
response.end(answer?.data); response.end(answer?.data);
}; };
export {invoke, getProcessBlob}; /** @} */

View File

@@ -1,8 +1,14 @@
/** /**
* TODOC * \file
* TODO: document so we can improve this * \defgroup tfhttp Tilde Friends HTTP Client JS
* @param {*} url * Tilde Friends server-side HTTP client.
* @returns * @{
*/
/**
* Parse a URL into protocol, host, path, and port parts.
* @param url
* @return An object of the URL parts.
*/ */
function parseUrl(url) { function parseUrl(url) {
// XXX: Hack. // XXX: Hack.
@@ -16,9 +22,9 @@ function parseUrl(url) {
} }
/** /**
* TODOC * Parse an HTTP response into headers and body content.
* @param {*} data * @param data The response data, headers and body included.
* @returns * @return headers and body data.
*/ */
function parseResponse(data) { function parseResponse(data) {
let firstLine; let firstLine;
@@ -36,15 +42,15 @@ function parseResponse(data) {
headers[line.substring(colon)] = line.substring(colon + 1); headers[line.substring(colon)] = line.substring(colon + 1);
} }
} }
return {body: data}; return {headers: headers, body: data};
} }
/** /**
* TODOC * Make an HTTP request.
* @param {*} url * @param url The URL.
* @param {*} options * @param options Request options.
* @param {*} allowed_hosts * @param allowed_hosts List of allowed hosts.
* @returns * @return A promise resolved with the response headers and body.
*/ */
export function fetch(url, options, allowed_hosts) { export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url); let parsed = parseUrl(url);
@@ -111,3 +117,5 @@ export function fetch(url, options, allowed_hosts) {
}); });
}); });
} }
/** @} */

View File

@@ -1,11 +1,22 @@
/**
* \file
* \defgroup tfrpc Tilde Friends RPC.
* Tilde Friends RPC.
* @{
*/
/** Whether this module is being run in a web browser. */
const k_is_browser = get_is_browser(); const k_is_browser = get_is_browser();
/** Registered methods. */
let g_api = {}; let g_api = {};
/** The next method identifier. */
let g_next_id = 1; let g_next_id = 1;
/** Identifiers of pending calls. */
let g_calls = {}; let g_calls = {};
/** /**
* TODOC * Check if being called from a browser vs. server-side.
* @returns * @return true if called from a browser.
*/ */
function get_is_browser() { function get_is_browser() {
try { try {
@@ -15,16 +26,30 @@ function get_is_browser() {
} }
} }
/** \cond */
if (k_is_browser) { if (k_is_browser) {
print = console.log; print = console.log;
} }
if (k_is_browser) {
window.addEventListener('message', function (event) {
call_rpc(event.data);
});
} else {
core.register('message', function (message) {
call_rpc(message?.message);
});
}
export let rpc = new Proxy({}, {get: make_rpc});
/** \endcond */
/** /**
* TODOC * Make a function to invoke a remote procedure.
* @param {*} target * @param target The target.
* @param {*} prop * @param prop The name of the function.
* @param {*} receiver * @param receiver The receiver.
* @returns * @return A function.
*/ */
function make_rpc(target, prop, receiver) { function make_rpc(target, prop, receiver) {
return function () { return function () {
@@ -55,8 +80,8 @@ function make_rpc(target, prop, receiver) {
} }
/** /**
* TODOC * Send a response.
* @param {*} response * @param response The response.
*/ */
function send(response) { function send(response) {
if (k_is_browser) { if (k_is_browser) {
@@ -67,8 +92,8 @@ function send(response) {
} }
/** /**
* TODOC * Invoke a remote procedure.
* @param {*} message * @param message An object describing the call.
*/ */
function call_rpc(message) { function call_rpc(message) {
if (message && message.message === 'tfrpc') { if (message && message.message === 'tfrpc') {
@@ -112,22 +137,12 @@ function call_rpc(message) {
} }
} }
if (k_is_browser) {
window.addEventListener('message', function (event) {
call_rpc(event.data);
});
} else {
core.register('message', function (message) {
call_rpc(message?.message);
});
}
export let rpc = new Proxy({}, {get: make_rpc});
/** /**
* TODOC * Register a function that to be called remotely.
* @param {*} method * @param method The method.
*/ */
export function register(method) { export function register(method) {
g_api[method.name] = method; g_api[method.name] = method;
} }
/** @} */

View File

@@ -25,14 +25,14 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.0.32"; version = "0.2025.8";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
rev = "v${version}"; rev = "v${version}";
hash = "sha256-Dk0NOEQIg2LeENySK0+MgpZEtfsClGq6dZL+eOOpE0U="; hash = "sha256-N/5lp8RL19B6Z43kRxx7c01WVJkK44a/wwNgRJPk5uI=";
fetchSubmodules = true; fetchSubmodules = true;
}; };

File diff suppressed because one or more lines are too long

379
deps/codemirror_src/package-lock.json generated vendored
View File

@@ -19,9 +19,10 @@
} }
}, },
"node_modules/@codemirror/autocomplete": { "node_modules/@codemirror/autocomplete": {
"version": "6.18.6", "version": "6.18.7",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.7.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", "integrity": "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -33,6 +34,7 @@
"version": "6.8.1", "version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.0",
@@ -44,6 +46,7 @@
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
@@ -56,6 +59,7 @@
"version": "6.4.9", "version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0", "@codemirror/lang-css": "^6.0.0",
@@ -72,6 +76,7 @@
"version": "6.2.4", "version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0", "@codemirror/language": "^6.6.0",
@@ -86,15 +91,17 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.11.2", "version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0", "@codemirror/view": "^6.23.0",
@@ -108,6 +115,7 @@
"version": "6.8.5", "version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0", "@codemirror/view": "^6.35.0",
@@ -118,6 +126,7 @@
"version": "6.5.11", "version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
@@ -128,6 +137,7 @@
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
@@ -136,6 +146,7 @@
"version": "6.1.3", "version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.0.0", "@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -144,9 +155,10 @@
} }
}, },
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.38.0", "version": "6.38.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.2.tgz",
"integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", "integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
"crelt": "^1.0.6", "crelt": "^1.0.6",
@@ -155,17 +167,14 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24" "@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
@@ -173,40 +182,35 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true, "dev": true,
"engines": { "license": "MIT",
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/source-map": { "node_modules/@jridgewell/source-map": {
"version": "0.3.6", "version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25" "@jridgewell/trace-mapping": "^0.3.25"
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.30",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
@@ -215,12 +219,14 @@
"node_modules/@lezer/common": { "node_modules/@lezer/common": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
}, },
"node_modules/@lezer/css": { "node_modules/@lezer/css": {
"version": "1.2.1", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
"integrity": "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg==", "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@@ -231,6 +237,7 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@@ -239,6 +246,7 @@
"version": "1.3.10", "version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@@ -246,9 +254,10 @@
} }
}, },
"node_modules/@lezer/javascript": { "node_modules/@lezer/javascript": {
"version": "1.5.1", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.3.tgz",
"integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", "integrity": "sha512-jexmlKq5NpGiB7t+0QkyhSXRgaiab5YisHIQW9C7EcU19KSUsDguZe9WY+rmRDg34nXoNH2LQ4SxpC+aJUchSQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3", "@lezer/highlight": "^1.1.3",
@@ -259,6 +268,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.2.0", "@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
@@ -269,6 +279,7 @@
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
@@ -276,12 +287,14 @@
"node_modules/@marijn/find-cluster-break": { "node_modules/@marijn/find-cluster-break": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
}, },
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.1", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@rollup/pluginutils": "^5.0.1", "@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2", "@types/resolve": "1.20.2",
@@ -306,6 +319,7 @@
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"serialize-javascript": "^6.0.1", "serialize-javascript": "^6.0.1",
"smob": "^1.0.0", "smob": "^1.0.0",
@@ -324,9 +338,10 @@
} }
}, },
"node_modules/@rollup/pluginutils": { "node_modules/@rollup/pluginutils": {
"version": "5.2.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
"integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.0", "@types/estree": "^1.0.0",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
@@ -345,240 +360,273 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz",
"integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz",
"integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"android" "android"
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz",
"integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz",
"integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz",
"integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz",
"integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"freebsd" "freebsd"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz",
"integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz",
"integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz",
"integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz",
"integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz",
"integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz",
"integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz",
"integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz",
"integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz",
"integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz",
"integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz",
"integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz",
"integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz",
"integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz",
"integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz",
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@@ -587,18 +635,21 @@
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
}, },
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -610,12 +661,14 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/codemirror": { "node_modules/codemirror": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0", "@codemirror/commands": "^6.0.0",
@@ -630,17 +683,20 @@
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/crelt": { "node_modules/crelt": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
}, },
"node_modules/deepmerge": { "node_modules/deepmerge": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -648,13 +704,15 @@
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@@ -667,6 +725,7 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
@@ -675,6 +734,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@@ -686,6 +746,7 @@
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": { "dependencies": {
"hasown": "^2.0.2" "hasown": "^2.0.2"
}, },
@@ -699,17 +760,20 @@
"node_modules/is-module": { "node_modules/is-module": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
}, },
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -722,6 +786,7 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "^5.1.0" "safe-buffer": "^5.1.0"
} }
@@ -730,6 +795,7 @@
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.16.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
@@ -746,9 +812,10 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.44.1", "version": "4.50.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz",
"integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
@@ -760,26 +827,27 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm-eabi": "4.50.1",
"@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-android-arm64": "4.50.1",
"@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.50.1",
"@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-darwin-x64": "4.50.1",
"@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.50.1",
"@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.50.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.50.1",
"@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.50.1",
"@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.50.1",
"@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.50.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.50.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-ppc64-gnu": "4.50.1",
"@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.50.1",
"@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.50.1",
"@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.50.1",
"@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.50.1",
"@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.50.1",
"@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-openharmony-arm64": "4.50.1",
"@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.50.1",
"@rollup/rollup-win32-x64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.50.1",
"@rollup/rollup-win32-x64-msvc": "4.50.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -801,13 +869,15 @@
"type": "consulting", "type": "consulting",
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
] ],
"license": "MIT"
}, },
"node_modules/serialize-javascript": { "node_modules/serialize-javascript": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"randombytes": "^2.1.0" "randombytes": "^2.1.0"
} }
@@ -816,13 +886,15 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "dev": true,
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -832,6 +904,7 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
@@ -840,12 +913,14 @@
"node_modules/style-mod": { "node_modules/style-mod": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
}, },
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -854,13 +929,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.43.1", "version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.14.0", "acorn": "^8.15.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@@ -874,7 +950,8 @@
"node_modules/w3c-keyname": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico"> <link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head> </head>
<body> <body>
<script src="speedscope-VHEG2FVF.js"></script> <script src="speedscope-HCR63FMT.js"></script>

View File

@@ -1,3 +1,3 @@
speedscope@1.22.2 speedscope@1.23.1
Sat Feb 15 13:02:38 PST 2025 Mon Aug 11 11:43:09 PDT 2025
1c254dcb3e2b4f6d921340d20e972d9d27b788f4 0cec0f82c334aed6cf19d43cabeadcda0d95e0fc

File diff suppressed because one or more lines are too long

203
deps/sqlite/sqlite3.c vendored
View File

@@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
** This file is an amalgamation of many separate C source files from SQLite ** This file is an amalgamation of many separate C source files from SQLite
** version 3.50.2. By combining all the individual C code files into this ** version 3.50.4. By combining all the individual C code files into this
** single large file, the entire code can be compiled as a single translation ** single large file, the entire code can be compiled as a single translation
** unit. This allows many compilers to do optimizations that would not be ** unit. This allows many compilers to do optimizations that would not be
** possible if the files were compiled separately. Performance improvements ** possible if the files were compiled separately. Performance improvements
@@ -18,7 +18,7 @@
** separate file. This file contains only code for the core SQLite library. ** separate file. This file contains only code for the core SQLite library.
** **
** The content in this amalgamation comes from Fossil check-in ** The content in this amalgamation comes from Fossil check-in
** 2af157d77fb1304a74176eaee7fbc7c7e932 with changes in files: ** 4d8adfb30e03f9cf27f800a2c1ba3c48fb4c with changes in files:
** **
** **
*/ */
@@ -465,9 +465,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.50.2" #define SQLITE_VERSION "3.50.4"
#define SQLITE_VERSION_NUMBER 3050002 #define SQLITE_VERSION_NUMBER 3050004
#define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079" #define SQLITE_SOURCE_ID "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@@ -9377,13 +9377,13 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt> ** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt>
** <dd>^This is the number of sort operations that have occurred. ** <dd>^This is the number of sort operations that have occurred.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance through careful use of indices.</dd> ** improve performance through careful use of indices.</dd>
** **
** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt> ** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt>
** <dd>^This is the number of rows inserted into transient indices that ** <dd>^This is the number of rows inserted into transient indices that
** were created automatically in order to help joins run faster. ** were created automatically in order to help joins run faster.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance by adding permanent indices that do not ** improve performance by adding permanent indices that do not
** need to be reinitialized each time the statement is run.</dd> ** need to be reinitialized each time the statement is run.</dd>
** **
** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt> ** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt>
@@ -9392,19 +9392,19 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** to 2147483647. The number of virtual machine operations can be ** to 2147483647. The number of virtual machine operations can be
** used as a proxy for the total work done by the prepared statement. ** used as a proxy for the total work done by the prepared statement.
** If the number of virtual machine operations exceeds 2147483647 ** If the number of virtual machine operations exceeds 2147483647
** then the value returned by this statement status code is undefined. ** then the value returned by this statement status code is undefined.</dd>
** **
** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt> ** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt>
** <dd>^This is the number of times that the prepare statement has been ** <dd>^This is the number of times that the prepare statement has been
** automatically regenerated due to schema changes or changes to ** automatically regenerated due to schema changes or changes to
** [bound parameters] that might affect the query plan. ** [bound parameters] that might affect the query plan.</dd>
** **
** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt> ** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt>
** <dd>^This is the number of times that the prepared statement has ** <dd>^This is the number of times that the prepared statement has
** been run. A single "run" for the purposes of this counter is one ** been run. A single "run" for the purposes of this counter is one
** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()]. ** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()].
** The counter is incremented on the first [sqlite3_step()] call of each ** The counter is incremented on the first [sqlite3_step()] call of each
** cycle. ** cycle.</dd>
** **
** [[SQLITE_STMTSTATUS_FILTER_MISS]] ** [[SQLITE_STMTSTATUS_FILTER_MISS]]
** [[SQLITE_STMTSTATUS_FILTER HIT]] ** [[SQLITE_STMTSTATUS_FILTER HIT]]
@@ -9414,7 +9414,7 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** step was bypassed because a Bloom filter returned not-found. The ** step was bypassed because a Bloom filter returned not-found. The
** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of ** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of
** times that the Bloom filter returned a find, and thus the join step ** times that the Bloom filter returned a find, and thus the join step
** had to be processed as normal. ** had to be processed as normal.</dd>
** **
** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt> ** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt>
** <dd>^This is the approximate number of bytes of heap memory ** <dd>^This is the approximate number of bytes of heap memory
@@ -9519,9 +9519,9 @@ struct sqlite3_pcache_page {
** SQLite will typically create one cache instance for each open database file, ** SQLite will typically create one cache instance for each open database file,
** though this is not guaranteed. ^The ** though this is not guaranteed. ^The
** first parameter, szPage, is the size in bytes of the pages that must ** first parameter, szPage, is the size in bytes of the pages that must
** be allocated by the cache. ^szPage will always a power of two. ^The ** be allocated by the cache. ^szPage will always be a power of two. ^The
** second parameter szExtra is a number of bytes of extra storage ** second parameter szExtra is a number of bytes of extra storage
** associated with each page cache entry. ^The szExtra parameter will ** associated with each page cache entry. ^The szExtra parameter will be
** a number less than 250. SQLite will use the ** a number less than 250. SQLite will use the
** extra szExtra bytes on each page to store metadata about the underlying ** extra szExtra bytes on each page to store metadata about the underlying
** database page on disk. The value passed into szExtra depends ** database page on disk. The value passed into szExtra depends
@@ -9529,17 +9529,17 @@ struct sqlite3_pcache_page {
** ^The third argument to xCreate(), bPurgeable, is true if the cache being ** ^The third argument to xCreate(), bPurgeable, is true if the cache being
** created will be used to cache database pages of a file stored on disk, or ** created will be used to cache database pages of a file stored on disk, or
** false if it is used for an in-memory database. The cache implementation ** false if it is used for an in-memory database. The cache implementation
** does not have to do anything special based with the value of bPurgeable; ** does not have to do anything special based upon the value of bPurgeable;
** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will ** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will
** never invoke xUnpin() except to deliberately delete a page. ** never invoke xUnpin() except to deliberately delete a page.
** ^In other words, calls to xUnpin() on a cache with bPurgeable set to ** ^In other words, calls to xUnpin() on a cache with bPurgeable set to
** false will always have the "discard" flag set to true. ** false will always have the "discard" flag set to true.
** ^Hence, a cache created with bPurgeable false will ** ^Hence, a cache created with bPurgeable set to false will
** never contain any unpinned pages. ** never contain any unpinned pages.
** **
** [[the xCachesize() page cache method]] ** [[the xCachesize() page cache method]]
** ^(The xCachesize() method may be called at any time by SQLite to set the ** ^(The xCachesize() method may be called at any time by SQLite to set the
** suggested maximum cache-size (number of pages stored by) the cache ** suggested maximum cache-size (number of pages stored) for the cache
** instance passed as the first argument. This is the value configured using ** instance passed as the first argument. This is the value configured using
** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable ** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable
** parameter, the implementation is not required to do anything with this ** parameter, the implementation is not required to do anything with this
@@ -9566,12 +9566,12 @@ struct sqlite3_pcache_page {
** implementation must return a pointer to the page buffer with its content ** implementation must return a pointer to the page buffer with its content
** intact. If the requested page is not already in the cache, then the ** intact. If the requested page is not already in the cache, then the
** cache implementation should use the value of the createFlag ** cache implementation should use the value of the createFlag
** parameter to help it determined what action to take: ** parameter to help it determine what action to take:
** **
** <table border=1 width=85% align=center> ** <table border=1 width=85% align=center>
** <tr><th> createFlag <th> Behavior when page is not already in cache ** <tr><th> createFlag <th> Behavior when page is not already in cache
** <tr><td> 0 <td> Do not allocate a new page. Return NULL. ** <tr><td> 0 <td> Do not allocate a new page. Return NULL.
** <tr><td> 1 <td> Allocate a new page if it easy and convenient to do so. ** <tr><td> 1 <td> Allocate a new page if it is easy and convenient to do so.
** Otherwise return NULL. ** Otherwise return NULL.
** <tr><td> 2 <td> Make every effort to allocate a new page. Only return ** <tr><td> 2 <td> Make every effort to allocate a new page. Only return
** NULL if allocating a new page is effectively impossible. ** NULL if allocating a new page is effectively impossible.
@@ -9588,7 +9588,7 @@ struct sqlite3_pcache_page {
** as its second argument. If the third parameter, discard, is non-zero, ** as its second argument. If the third parameter, discard, is non-zero,
** then the page must be evicted from the cache. ** then the page must be evicted from the cache.
** ^If the discard parameter is ** ^If the discard parameter is
** zero, then the page may be discarded or retained at the discretion of ** zero, then the page may be discarded or retained at the discretion of the
** page cache implementation. ^The page cache implementation ** page cache implementation. ^The page cache implementation
** may choose to evict unpinned pages at any time. ** may choose to evict unpinned pages at any time.
** **
@@ -9606,7 +9606,7 @@ struct sqlite3_pcache_page {
** When SQLite calls the xTruncate() method, the cache must discard all ** When SQLite calls the xTruncate() method, the cache must discard all
** existing cache entries with page numbers (keys) greater than or equal ** existing cache entries with page numbers (keys) greater than or equal
** to the value of the iLimit parameter passed to xTruncate(). If any ** to the value of the iLimit parameter passed to xTruncate(). If any
** of these pages are pinned, they are implicitly unpinned, meaning that ** of these pages are pinned, they become implicitly unpinned, meaning that
** they can be safely discarded. ** they can be safely discarded.
** **
** [[the xDestroy() page cache method]] ** [[the xDestroy() page cache method]]
@@ -9905,7 +9905,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** application receives an SQLITE_LOCKED error, it may call the ** application receives an SQLITE_LOCKED error, it may call the
** sqlite3_unlock_notify() method with the blocked connection handle as ** sqlite3_unlock_notify() method with the blocked connection handle as
** the first argument to register for a callback that will be invoked ** the first argument to register for a callback that will be invoked
** when the blocking connections current transaction is concluded. ^The ** when the blocking connection's current transaction is concluded. ^The
** callback is invoked from within the [sqlite3_step] or [sqlite3_close] ** callback is invoked from within the [sqlite3_step] or [sqlite3_close]
** call that concludes the blocking connection's transaction. ** call that concludes the blocking connection's transaction.
** **
@@ -9925,7 +9925,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** blocked connection already has a registered unlock-notify callback, ** blocked connection already has a registered unlock-notify callback,
** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is ** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is
** called with a NULL pointer as its second argument, then any existing ** called with a NULL pointer as its second argument, then any existing
** unlock-notify callback is canceled. ^The blocked connections ** unlock-notify callback is canceled. ^The blocked connection's
** unlock-notify callback may also be canceled by closing the blocked ** unlock-notify callback may also be canceled by closing the blocked
** connection using [sqlite3_close()]. ** connection using [sqlite3_close()].
** **
@@ -10323,7 +10323,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** support constraints. In this configuration (which is the default) if ** support constraints. In this configuration (which is the default) if
** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire ** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire
** statement is rolled back as if [ON CONFLICT | OR ABORT] had been ** statement is rolled back as if [ON CONFLICT | OR ABORT] had been
** specified as part of the users SQL statement, regardless of the actual ** specified as part of the user's SQL statement, regardless of the actual
** ON CONFLICT mode specified. ** ON CONFLICT mode specified.
** **
** If X is non-zero, then the virtual table implementation guarantees ** If X is non-zero, then the virtual table implementation guarantees
@@ -10357,7 +10357,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt> ** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt>
** <dd>Calls of the form ** <dd>Calls of the form
** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the ** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the
** the [xConnect] or [xCreate] methods of a [virtual table] implementation ** [xConnect] or [xCreate] methods of a [virtual table] implementation
** identify that virtual table as being safe to use from within triggers ** identify that virtual table as being safe to use from within triggers
** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the ** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the
** virtual table can do no serious harm even if it is controlled by a ** virtual table can do no serious harm even if it is controlled by a
@@ -10525,7 +10525,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** </table> ** </table>
** **
** ^For the purposes of comparing virtual table output values to see if the ** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered ** values are the same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS" ** to be the same. In other words, the comparison operator is "IS"
** (or "IS NOT DISTINCT FROM") and not "==". ** (or "IS NOT DISTINCT FROM") and not "==".
** **
@@ -10535,7 +10535,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** **
** ^A virtual table implementation is always free to return rows in any order ** ^A virtual table implementation is always free to return rows in any order
** it wants, as long as the "orderByConsumed" flag is not set. ^When the ** it wants, as long as the "orderByConsumed" flag is not set. ^When the
** the "orderByConsumed" flag is unset, the query planner will add extra ** "orderByConsumed" flag is unset, the query planner will add extra
** [bytecode] to ensure that the final results returned by the SQL query are ** [bytecode] to ensure that the final results returned by the SQL query are
** ordered correctly. The use of the "orderByConsumed" flag and the ** ordered correctly. The use of the "orderByConsumed" flag and the
** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful ** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful
@@ -10632,7 +10632,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** sqlite3_vtab_in_next(X,P) should be one of the parameters to the ** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
** xFilter method which invokes these routines, and specifically ** xFilter method which invokes these routines, and specifically
** a parameter that was previously selected for all-at-once IN constraint ** a parameter that was previously selected for all-at-once IN constraint
** processing use the [sqlite3_vtab_in()] interface in the ** processing using the [sqlite3_vtab_in()] interface in the
** [xBestIndex|xBestIndex method]. ^(If the X parameter is not ** [xBestIndex|xBestIndex method]. ^(If the X parameter is not
** an xFilter argument that was selected for all-at-once IN constraint ** an xFilter argument that was selected for all-at-once IN constraint
** processing, then these routines return [SQLITE_ERROR].)^ ** processing, then these routines return [SQLITE_ERROR].)^
@@ -10687,7 +10687,7 @@ SQLITE_API int sqlite3_vtab_in_next(sqlite3_value *pVal, sqlite3_value **ppOut);
** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V) ** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V)
** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th ** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th
** constraint is not available. ^The sqlite3_vtab_rhs_value() interface ** constraint is not available. ^The sqlite3_vtab_rhs_value() interface
** can return an result code other than SQLITE_OK or SQLITE_NOTFOUND if ** can return a result code other than SQLITE_OK or SQLITE_NOTFOUND if
** something goes wrong. ** something goes wrong.
** **
** The sqlite3_vtab_rhs_value() interface is usually only successful if ** The sqlite3_vtab_rhs_value() interface is usually only successful if
@@ -10715,8 +10715,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** KEYWORDS: {conflict resolution mode} ** KEYWORDS: {conflict resolution mode}
** **
** These constants are returned by [sqlite3_vtab_on_conflict()] to ** These constants are returned by [sqlite3_vtab_on_conflict()] to
** inform a [virtual table] implementation what the [ON CONFLICT] mode ** inform a [virtual table] implementation of the [ON CONFLICT] mode
** is for the SQL statement being evaluated. ** for the SQL statement being evaluated.
** **
** Note that the [SQLITE_IGNORE] constant is also used as a potential ** Note that the [SQLITE_IGNORE] constant is also used as a potential
** return value from the [sqlite3_set_authorizer()] callback and that ** return value from the [sqlite3_set_authorizer()] callback and that
@@ -10756,39 +10756,39 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt> ** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt>
** <dd>^The "double" variable pointed to by the V parameter will be set to the ** <dd>^The "double" variable pointed to by the V parameter will be set to the
** query planner's estimate for the average number of rows output from each ** query planner's estimate for the average number of rows output from each
** iteration of the X-th loop. If the query planner's estimates was accurate, ** iteration of the X-th loop. If the query planner's estimate was accurate,
** then this value will approximate the quotient NVISIT/NLOOP and the ** then this value will approximate the quotient NVISIT/NLOOP and the
** product of this value for all prior loops with the same SELECTID will ** product of this value for all prior loops with the same SELECTID will
** be the NLOOP value for the current loop. ** be the NLOOP value for the current loop.</dd>
** **
** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt> ** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the name of the index or table ** to a zero-terminated UTF-8 string containing the name of the index or table
** used for the X-th loop. ** used for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt> ** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN] ** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
** description for the X-th loop. ** description for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt> ** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
** <dd>^The "int" variable pointed to by the V parameter will be set to the ** <dd>^The "int" variable pointed to by the V parameter will be set to the
** id for the X-th query plan element. The id value is unique within the ** id for the X-th query plan element. The id value is unique within the
** statement. The select-id is the same value as is output in the first ** statement. The select-id is the same value as is output in the first
** column of an [EXPLAIN QUERY PLAN] query. ** column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt> ** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
** <dd>The "int" variable pointed to by the V parameter will be set to the ** <dd>The "int" variable pointed to by the V parameter will be set to the
** the id of the parent of the current query element, if applicable, or ** id of the parent of the current query element, if applicable, or
** to zero if the query element has no parent. This is the same value as ** to zero if the query element has no parent. This is the same value as
** returned in the second column of an [EXPLAIN QUERY PLAN] query. ** returned in the second column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt> ** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
** <dd>The sqlite3_int64 output value is set to the number of cycles, ** <dd>The sqlite3_int64 output value is set to the number of cycles,
** according to the processor time-stamp counter, that elapsed while the ** according to the processor time-stamp counter, that elapsed while the
** query element was being processed. This value is not available for ** query element was being processed. This value is not available for
** all query elements - if it is unavailable the output variable is ** all query elements - if it is unavailable the output variable is
** set to -1. ** set to -1.</dd>
** </dl> ** </dl>
*/ */
#define SQLITE_SCANSTAT_NLOOP 0 #define SQLITE_SCANSTAT_NLOOP 0
@@ -10829,8 +10829,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter. ** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
** **
** Parameter "idx" identifies the specific query element to retrieve statistics ** Parameter "idx" identifies the specific query element to retrieve statistics
** for. Query elements are numbered starting from zero. A value of -1 may be ** for. Query elements are numbered starting from zero. A value of -1 may
** to query for statistics regarding the entire query. ^If idx is out of range ** retrieve statistics for the entire query. ^If idx is out of range
** - less than -1 or greater than or equal to the total number of query ** - less than -1 or greater than or equal to the total number of query
** elements used to implement the statement - a non-zero value is returned and ** elements used to implement the statement - a non-zero value is returned and
** the variable that pOut points to is unchanged. ** the variable that pOut points to is unchanged.
@@ -10987,8 +10987,8 @@ SQLITE_API int sqlite3_db_cacheflush(sqlite3*);
** triggers; and so forth. ** triggers; and so forth.
** **
** When the [sqlite3_blob_write()] API is used to update a blob column, ** When the [sqlite3_blob_write()] API is used to update a blob column,
** the pre-update hook is invoked with SQLITE_DELETE. This is because the ** the pre-update hook is invoked with SQLITE_DELETE, because
** in this case the new values are not available. In this case, when a ** the new values are not yet available. In this case, when a
** callback made with op==SQLITE_DELETE is actually a write using the ** callback made with op==SQLITE_DELETE is actually a write using the
** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns ** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns
** the index of the column being written. In other cases, where the ** the index of the column being written. In other cases, where the
@@ -11241,7 +11241,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** For an ordinary on-disk database file, the serialization is just a ** For an ordinary on-disk database file, the serialization is just a
** copy of the disk file. For an in-memory database or a "TEMP" database, ** copy of the disk file. For an in-memory database or a "TEMP" database,
** the serialization is the same sequence of bytes which would be written ** the serialization is the same sequence of bytes which would be written
** to disk if that database where backed up to disk. ** to disk if that database were backed up to disk.
** **
** The usual case is that sqlite3_serialize() copies the serialization of ** The usual case is that sqlite3_serialize() copies the serialization of
** the database into memory obtained from [sqlite3_malloc64()] and returns ** the database into memory obtained from [sqlite3_malloc64()] and returns
@@ -11250,7 +11250,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations ** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations
** are made, and the sqlite3_serialize() function will return a pointer ** are made, and the sqlite3_serialize() function will return a pointer
** to the contiguous memory representation of the database that SQLite ** to the contiguous memory representation of the database that SQLite
** is currently using for that database, or NULL if the no such contiguous ** is currently using for that database, or NULL if no such contiguous
** memory representation of the database exists. A contiguous memory ** memory representation of the database exists. A contiguous memory
** representation of the database will usually only exist if there has ** representation of the database will usually only exist if there has
** been a prior call to [sqlite3_deserialize(D,S,...)] with the same ** been a prior call to [sqlite3_deserialize(D,S,...)] with the same
@@ -11321,7 +11321,7 @@ SQLITE_API unsigned char *sqlite3_serialize(
** database is currently in a read transaction or is involved in a backup ** database is currently in a read transaction or is involved in a backup
** operation. ** operation.
** **
** It is not possible to deserialized into the TEMP database. If the ** It is not possible to deserialize into the TEMP database. If the
** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the ** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the
** function returns SQLITE_ERROR. ** function returns SQLITE_ERROR.
** **
@@ -11343,7 +11343,7 @@ SQLITE_API int sqlite3_deserialize(
sqlite3 *db, /* The database connection */ sqlite3 *db, /* The database connection */
const char *zSchema, /* Which DB to reopen with the deserialization */ const char *zSchema, /* Which DB to reopen with the deserialization */
unsigned char *pData, /* The serialized database content */ unsigned char *pData, /* The serialized database content */
sqlite3_int64 szDb, /* Number bytes in the deserialization */ sqlite3_int64 szDb, /* Number of bytes in the deserialization */
sqlite3_int64 szBuf, /* Total size of buffer pData[] */ sqlite3_int64 szBuf, /* Total size of buffer pData[] */
unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */ unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */
); );
@@ -11351,7 +11351,7 @@ SQLITE_API int sqlite3_deserialize(
/* /*
** CAPI3REF: Flags for sqlite3_deserialize() ** CAPI3REF: Flags for sqlite3_deserialize()
** **
** The following are allowed values for 6th argument (the F argument) to ** The following are allowed values for the 6th argument (the F argument) to
** the [sqlite3_deserialize(D,S,P,N,M,F)] interface. ** the [sqlite3_deserialize(D,S,P,N,M,F)] interface.
** **
** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization ** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization
@@ -19168,7 +19168,6 @@ struct Index {
unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */ unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */
unsigned bNoQuery:1; /* Do not use this index to optimize queries */ unsigned bNoQuery:1; /* Do not use this index to optimize queries */
unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */ unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */
unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */
unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */ unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */
unsigned bHasExpr:1; /* Index contains an expression, either a literal unsigned bHasExpr:1; /* Index contains an expression, either a literal
** expression, or a reference to a VIRTUAL column */ ** expression, or a reference to a VIRTUAL column */
@@ -19441,6 +19440,7 @@ struct Expr {
Table *pTab; /* TK_COLUMN: Table containing column. Can be NULL Table *pTab; /* TK_COLUMN: Table containing column. Can be NULL
** for a column of an index on an expression */ ** for a column of an index on an expression */
Window *pWin; /* EP_WinFunc: Window/Filter defn for a function */ Window *pWin; /* EP_WinFunc: Window/Filter defn for a function */
int nReg; /* TK_NULLS: Number of registers to NULL out */
struct { /* TK_IN, TK_SELECT, and TK_EXISTS */ struct { /* TK_IN, TK_SELECT, and TK_EXISTS */
int iAddr; /* Subroutine entry address */ int iAddr; /* Subroutine entry address */
int regReturn; /* Register used to hold return address */ int regReturn; /* Register used to hold return address */
@@ -21475,6 +21475,7 @@ SQLITE_PRIVATE void sqlite3ExprCodeGeneratedColumn(Parse*, Table*, Column*, int)
SQLITE_PRIVATE void sqlite3ExprCodeCopy(Parse*, Expr*, int); SQLITE_PRIVATE void sqlite3ExprCodeCopy(Parse*, Expr*, int);
SQLITE_PRIVATE void sqlite3ExprCodeFactorable(Parse*, Expr*, int); SQLITE_PRIVATE void sqlite3ExprCodeFactorable(Parse*, Expr*, int);
SQLITE_PRIVATE int sqlite3ExprCodeRunJustOnce(Parse*, Expr*, int); SQLITE_PRIVATE int sqlite3ExprCodeRunJustOnce(Parse*, Expr*, int);
SQLITE_PRIVATE void sqlite3ExprNullRegisterRange(Parse*, int, int);
SQLITE_PRIVATE int sqlite3ExprCodeTemp(Parse*, Expr*, int*); SQLITE_PRIVATE int sqlite3ExprCodeTemp(Parse*, Expr*, int*);
SQLITE_PRIVATE int sqlite3ExprCodeTarget(Parse*, Expr*, int); SQLITE_PRIVATE int sqlite3ExprCodeTarget(Parse*, Expr*, int);
SQLITE_PRIVATE int sqlite3ExprCodeExprList(Parse*, ExprList*, int, int, u8); SQLITE_PRIVATE int sqlite3ExprCodeExprList(Parse*, ExprList*, int, int, u8);
@@ -111488,7 +111489,7 @@ SQLITE_PRIVATE Expr *sqlite3ExprAnd(Parse *pParse, Expr *pLeft, Expr *pRight){
return pLeft; return pLeft;
}else{ }else{
u32 f = pLeft->flags | pRight->flags; u32 f = pLeft->flags | pRight->flags;
if( (f&(EP_OuterON|EP_InnerON|EP_IsFalse))==EP_IsFalse if( (f&(EP_OuterON|EP_InnerON|EP_IsFalse|EP_HasFunc))==EP_IsFalse
&& !IN_RENAME_OBJECT && !IN_RENAME_OBJECT
){ ){
sqlite3ExprDeferredDelete(pParse, pLeft); sqlite3ExprDeferredDelete(pParse, pLeft);
@@ -115242,6 +115243,12 @@ expr_code_doover:
sqlite3VdbeLoadString(v, target, pExpr->u.zToken); sqlite3VdbeLoadString(v, target, pExpr->u.zToken);
return target; return target;
} }
case TK_NULLS: {
/* Set a range of registers to NULL. pExpr->y.nReg registers starting
** with target */
sqlite3VdbeAddOp3(v, OP_Null, 0, target, target + pExpr->y.nReg - 1);
return target;
}
default: { default: {
/* Make NULL the default case so that if a bug causes an illegal /* Make NULL the default case so that if a bug causes an illegal
** Expr node to be passed into this function, it will be handled ** Expr node to be passed into this function, it will be handled
@@ -115926,6 +115933,25 @@ SQLITE_PRIVATE int sqlite3ExprCodeRunJustOnce(
return regDest; return regDest;
} }
/*
** Make arrangements to invoke OP_Null on a range of registers
** during initialization.
*/
SQLITE_PRIVATE SQLITE_NOINLINE void sqlite3ExprNullRegisterRange(
Parse *pParse, /* Parsing context */
int iReg, /* First register to set to NULL */
int nReg /* Number of sequential registers to NULL out */
){
u8 okConstFactor = pParse->okConstFactor;
Expr t;
memset(&t, 0, sizeof(t));
t.op = TK_NULLS;
t.y.nReg = nReg;
pParse->okConstFactor = 1;
sqlite3ExprCodeRunJustOnce(pParse, &t, iReg);
pParse->okConstFactor = okConstFactor;
}
/* /*
** Generate code to evaluate an expression and store the results ** Generate code to evaluate an expression and store the results
** into a register. Return the register number where the results ** into a register. Return the register number where the results
@@ -127196,7 +127222,6 @@ SQLITE_PRIVATE void sqlite3CreateIndex(
assert( j<=0x7fff ); assert( j<=0x7fff );
if( j<0 ){ if( j<0 ){
j = pTab->iPKey; j = pTab->iPKey;
pIndex->bIdxRowid = 1;
}else{ }else{
if( pTab->aCol[j].notNull==0 ){ if( pTab->aCol[j].notNull==0 ){
pIndex->uniqNotNull = 0; pIndex->uniqNotNull = 0;
@@ -153177,6 +153202,7 @@ SQLITE_PRIVATE int sqlite3Select(
sqlite3VdbeAddOp2(v, OP_Integer, 0, iAbortFlag); sqlite3VdbeAddOp2(v, OP_Integer, 0, iAbortFlag);
VdbeComment((v, "clear abort flag")); VdbeComment((v, "clear abort flag"));
sqlite3VdbeAddOp3(v, OP_Null, 0, iAMem, iAMem+pGroupBy->nExpr-1); sqlite3VdbeAddOp3(v, OP_Null, 0, iAMem, iAMem+pGroupBy->nExpr-1);
sqlite3ExprNullRegisterRange(pParse, iAMem, pGroupBy->nExpr);
/* Begin a loop that will extract all source rows in GROUP BY order. /* Begin a loop that will extract all source rows in GROUP BY order.
** This might involve two separate loops with an OP_Sort in between, or ** This might involve two separate loops with an OP_Sort in between, or
@@ -160173,7 +160199,9 @@ static Expr *removeUnindexableInClauseTerms(
int iField; int iField;
assert( (pLoop->aLTerm[i]->eOperator & (WO_OR|WO_AND))==0 ); assert( (pLoop->aLTerm[i]->eOperator & (WO_OR|WO_AND))==0 );
iField = pLoop->aLTerm[i]->u.x.iField - 1; iField = pLoop->aLTerm[i]->u.x.iField - 1;
if( pOrigRhs->a[iField].pExpr==0 ) continue; /* Duplicate PK column */ if( NEVER(pOrigRhs->a[iField].pExpr==0) ){
continue; /* Duplicate PK column */
}
pRhs = sqlite3ExprListAppend(pParse, pRhs, pOrigRhs->a[iField].pExpr); pRhs = sqlite3ExprListAppend(pParse, pRhs, pOrigRhs->a[iField].pExpr);
pOrigRhs->a[iField].pExpr = 0; pOrigRhs->a[iField].pExpr = 0;
if( pRhs ) pRhs->a[pRhs->nExpr-1].u.x.iOrderByCol = iField+1; if( pRhs ) pRhs->a[pRhs->nExpr-1].u.x.iOrderByCol = iField+1;
@@ -160270,7 +160298,7 @@ static SQLITE_NOINLINE void codeINTerm(
return; return;
} }
} }
for(i=iEq;i<pLoop->nLTerm; i++){ for(i=iEq; i<pLoop->nLTerm; i++){
assert( pLoop->aLTerm[i]!=0 ); assert( pLoop->aLTerm[i]!=0 );
if( pLoop->aLTerm[i]->pExpr==pX ) nEq++; if( pLoop->aLTerm[i]->pExpr==pX ) nEq++;
} }
@@ -160279,22 +160307,13 @@ static SQLITE_NOINLINE void codeINTerm(
if( !ExprUseXSelect(pX) || pX->x.pSelect->pEList->nExpr==1 ){ if( !ExprUseXSelect(pX) || pX->x.pSelect->pEList->nExpr==1 ){
eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP, 0, 0, &iTab); eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP, 0, 0, &iTab);
}else{ }else{
Expr *pExpr = pTerm->pExpr; sqlite3 *db = pParse->db;
if( pExpr->iTable==0 || !ExprHasProperty(pExpr, EP_Subrtn) ){ Expr *pXMod = removeUnindexableInClauseTerms(pParse, iEq, pLoop, pX);
sqlite3 *db = pParse->db; if( !db->mallocFailed ){
pX = removeUnindexableInClauseTerms(pParse, iEq, pLoop, pX); aiMap = (int*)sqlite3DbMallocZero(db, sizeof(int)*nEq);
if( !db->mallocFailed ){ eType = sqlite3FindInIndex(pParse, pXMod, IN_INDEX_LOOP, 0, aiMap, &iTab);
aiMap = (int*)sqlite3DbMallocZero(pParse->db, sizeof(int)*nEq);
eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP, 0, aiMap,&iTab);
pExpr->iTable = iTab;
}
sqlite3ExprDelete(db, pX);
}else{
int n = sqlite3ExprVectorSize(pX->pLeft);
aiMap = (int*)sqlite3DbMallocZero(pParse->db, sizeof(int)*MAX(nEq,n));
eType = sqlite3FindInIndex(pParse, pX, IN_INDEX_LOOP, 0, aiMap, &iTab);
} }
pX = pExpr; sqlite3ExprDelete(db, pXMod);
} }
if( eType==IN_INDEX_INDEX_DESC ){ if( eType==IN_INDEX_INDEX_DESC ){
@@ -160324,7 +160343,7 @@ static SQLITE_NOINLINE void codeINTerm(
if( pIn ){ if( pIn ){
int iMap = 0; /* Index in aiMap[] */ int iMap = 0; /* Index in aiMap[] */
pIn += i; pIn += i;
for(i=iEq;i<pLoop->nLTerm; i++){ for(i=iEq; i<pLoop->nLTerm; i++){
if( pLoop->aLTerm[i]->pExpr==pX ){ if( pLoop->aLTerm[i]->pExpr==pX ){
int iOut = iTarget + i - iEq; int iOut = iTarget + i - iEq;
if( eType==IN_INDEX_ROWID ){ if( eType==IN_INDEX_ROWID ){
@@ -167682,6 +167701,7 @@ static int whereLoopAddBtreeIndex(
if( ExprUseXSelect(pExpr) ){ if( ExprUseXSelect(pExpr) ){
/* "x IN (SELECT ...)": TUNING: the SELECT returns 25 rows */ /* "x IN (SELECT ...)": TUNING: the SELECT returns 25 rows */
int i; int i;
int bRedundant = 0;
nIn = 46; assert( 46==sqlite3LogEst(25) ); nIn = 46; assert( 46==sqlite3LogEst(25) );
/* The expression may actually be of the form (x, y) IN (SELECT...). /* The expression may actually be of the form (x, y) IN (SELECT...).
@@ -167690,7 +167710,20 @@ static int whereLoopAddBtreeIndex(
** for each such term. The following loop checks that pTerm is the ** for each such term. The following loop checks that pTerm is the
** first such term in use, and sets nIn back to 0 if it is not. */ ** first such term in use, and sets nIn back to 0 if it is not. */
for(i=0; i<pNew->nLTerm-1; i++){ for(i=0; i<pNew->nLTerm-1; i++){
if( pNew->aLTerm[i] && pNew->aLTerm[i]->pExpr==pExpr ) nIn = 0; if( pNew->aLTerm[i] && pNew->aLTerm[i]->pExpr==pExpr ){
nIn = 0;
if( pNew->aLTerm[i]->u.x.iField == pTerm->u.x.iField ){
/* Detect when two or more columns of an index match the same
** column of a vector IN operater, and avoid adding the column
** to the WhereLoop more than once. See tag-20250707-01
** in test/rowvalue.test */
bRedundant = 1;
}
}
}
if( bRedundant ){
pNew->nLTerm--;
continue;
} }
}else if( ALWAYS(pExpr->x.pList && pExpr->x.pList->nExpr) ){ }else if( ALWAYS(pExpr->x.pList && pExpr->x.pList->nExpr) ){
/* "x IN (value, value, ...)" */ /* "x IN (value, value, ...)" */
@@ -167922,7 +167955,7 @@ static int whereLoopAddBtreeIndex(
if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
&& pNew->u.btree.nEq<pProbe->nColumn && pNew->u.btree.nEq<pProbe->nColumn
&& (pNew->u.btree.nEq<pProbe->nKeyCol || && (pNew->u.btree.nEq<pProbe->nKeyCol ||
(pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid)) pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY)
){ ){
if( pNew->u.btree.nEq>3 ){ if( pNew->u.btree.nEq>3 ){
sqlite3ProgressCheck(pParse); sqlite3ProgressCheck(pParse);
@@ -168465,6 +168498,7 @@ static int whereLoopAddBtree(
pNew->u.btree.nEq = 0; pNew->u.btree.nEq = 0;
pNew->u.btree.nBtm = 0; pNew->u.btree.nBtm = 0;
pNew->u.btree.nTop = 0; pNew->u.btree.nTop = 0;
pNew->u.btree.nDistinctCol = 0;
pNew->nSkip = 0; pNew->nSkip = 0;
pNew->nLTerm = 0; pNew->nLTerm = 0;
pNew->iSortIdx = 0; pNew->iSortIdx = 0;
@@ -169533,8 +169567,6 @@ static i8 wherePathSatisfiesOrderBy(
obSat = obDone; obSat = obDone;
} }
break; break;
}else if( wctrlFlags & WHERE_DISTINCTBY ){
pLoop->u.btree.nDistinctCol = 0;
} }
iCur = pWInfo->pTabList->a[pLoop->iTab].iCursor; iCur = pWInfo->pTabList->a[pLoop->iTab].iCursor;
@@ -179897,12 +179929,21 @@ static YYACTIONTYPE yy_reduce(
** expr1 IN () ** expr1 IN ()
** expr1 NOT IN () ** expr1 NOT IN ()
** **
** simplify to constants 0 (false) and 1 (true), respectively, ** simplify to constants 0 (false) and 1 (true), respectively.
** regardless of the value of expr1. **
** Except, do not apply this optimization if expr1 contains a function
** because that function might be an aggregate (we don't know yet whether
** it is or not) and if it is an aggregate, that could change the meaning
** of the whole query.
*/ */
sqlite3ExprUnmapAndDelete(pParse, yymsp[-4].minor.yy590); Expr *pB = sqlite3Expr(pParse->db, TK_STRING, yymsp[-3].minor.yy502 ? "true" : "false");
yymsp[-4].minor.yy590 = sqlite3Expr(pParse->db, TK_STRING, yymsp[-3].minor.yy502 ? "true" : "false"); if( pB ) sqlite3ExprIdToTrueFalse(pB);
if( yymsp[-4].minor.yy590 ) sqlite3ExprIdToTrueFalse(yymsp[-4].minor.yy590); if( !ExprHasProperty(yymsp[-4].minor.yy590, EP_HasFunc) ){
sqlite3ExprUnmapAndDelete(pParse, yymsp[-4].minor.yy590);
yymsp[-4].minor.yy590 = pB;
}else{
yymsp[-4].minor.yy590 = sqlite3PExpr(pParse, yymsp[-3].minor.yy502 ? TK_OR : TK_AND, pB, yymsp[-4].minor.yy590);
}
}else{ }else{
Expr *pRHS = yymsp[-1].minor.yy402->a[0].pExpr; Expr *pRHS = yymsp[-1].minor.yy402->a[0].pExpr;
if( yymsp[-1].minor.yy402->nExpr==1 && sqlite3ExprIsConstant(pParse,pRHS) && yymsp[-4].minor.yy590->op!=TK_VECTOR ){ if( yymsp[-1].minor.yy402->nExpr==1 && sqlite3ExprIsConstant(pParse,pRHS) && yymsp[-4].minor.yy590->op!=TK_VECTOR ){
@@ -181508,7 +181549,7 @@ static int getToken(const unsigned char **pz){
int t; /* Token type to return */ int t; /* Token type to return */
do { do {
z += sqlite3GetToken(z, &t); z += sqlite3GetToken(z, &t);
}while( t==TK_SPACE ); }while( t==TK_SPACE || t==TK_COMMENT );
if( t==TK_ID if( t==TK_ID
|| t==TK_STRING || t==TK_STRING
|| t==TK_JOIN_KW || t==TK_JOIN_KW
@@ -246163,9 +246204,9 @@ static void fts5SegIterSetNext(Fts5Index *p, Fts5SegIter *pIter){
** leave an error in the Fts5Index object. ** leave an error in the Fts5Index object.
*/ */
static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){ static void fts5SegIterAllocTombstone(Fts5Index *p, Fts5SegIter *pIter){
const int nTomb = pIter->pSeg->nPgTombstone; const i64 nTomb = (i64)pIter->pSeg->nPgTombstone;
if( nTomb>0 ){ if( nTomb>0 ){
int nByte = SZ_FTS5TOMBSTONEARRAY(nTomb+1); i64 nByte = SZ_FTS5TOMBSTONEARRAY(nTomb+1);
Fts5TombstoneArray *pNew; Fts5TombstoneArray *pNew;
pNew = (Fts5TombstoneArray*)sqlite3Fts5MallocZero(&p->rc, nByte); pNew = (Fts5TombstoneArray*)sqlite3Fts5MallocZero(&p->rc, nByte);
if( pNew ){ if( pNew ){
@@ -257266,7 +257307,7 @@ static void fts5SourceIdFunc(
){ ){
assert( nArg==0 ); assert( nArg==0 );
UNUSED_PARAM2(nArg, apUnused); UNUSED_PARAM2(nArg, apUnused);
sqlite3_result_text(pCtx, "fts5: 2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079", -1, SQLITE_TRANSIENT); sqlite3_result_text(pCtx, "fts5: 2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3", -1, SQLITE_TRANSIENT);
} }
/* /*

90
deps/sqlite/sqlite3.h vendored
View File

@@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.50.2" #define SQLITE_VERSION "3.50.4"
#define SQLITE_VERSION_NUMBER 3050002 #define SQLITE_VERSION_NUMBER 3050004
#define SQLITE_SOURCE_ID "2025-06-28 14:00:48 2af157d77fb1304a74176eaee7fbc7c7e932d946bf25325e9c26c91db19e3079" #define SQLITE_SOURCE_ID "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@@ -9058,13 +9058,13 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt> ** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt>
** <dd>^This is the number of sort operations that have occurred. ** <dd>^This is the number of sort operations that have occurred.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance through careful use of indices.</dd> ** improve performance through careful use of indices.</dd>
** **
** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt> ** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt>
** <dd>^This is the number of rows inserted into transient indices that ** <dd>^This is the number of rows inserted into transient indices that
** were created automatically in order to help joins run faster. ** were created automatically in order to help joins run faster.
** A non-zero value in this counter may indicate an opportunity to ** A non-zero value in this counter may indicate an opportunity to
** improvement performance by adding permanent indices that do not ** improve performance by adding permanent indices that do not
** need to be reinitialized each time the statement is run.</dd> ** need to be reinitialized each time the statement is run.</dd>
** **
** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt> ** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt>
@@ -9073,19 +9073,19 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** to 2147483647. The number of virtual machine operations can be ** to 2147483647. The number of virtual machine operations can be
** used as a proxy for the total work done by the prepared statement. ** used as a proxy for the total work done by the prepared statement.
** If the number of virtual machine operations exceeds 2147483647 ** If the number of virtual machine operations exceeds 2147483647
** then the value returned by this statement status code is undefined. ** then the value returned by this statement status code is undefined.</dd>
** **
** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt> ** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt>
** <dd>^This is the number of times that the prepare statement has been ** <dd>^This is the number of times that the prepare statement has been
** automatically regenerated due to schema changes or changes to ** automatically regenerated due to schema changes or changes to
** [bound parameters] that might affect the query plan. ** [bound parameters] that might affect the query plan.</dd>
** **
** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt> ** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt>
** <dd>^This is the number of times that the prepared statement has ** <dd>^This is the number of times that the prepared statement has
** been run. A single "run" for the purposes of this counter is one ** been run. A single "run" for the purposes of this counter is one
** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()]. ** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()].
** The counter is incremented on the first [sqlite3_step()] call of each ** The counter is incremented on the first [sqlite3_step()] call of each
** cycle. ** cycle.</dd>
** **
** [[SQLITE_STMTSTATUS_FILTER_MISS]] ** [[SQLITE_STMTSTATUS_FILTER_MISS]]
** [[SQLITE_STMTSTATUS_FILTER HIT]] ** [[SQLITE_STMTSTATUS_FILTER HIT]]
@@ -9095,7 +9095,7 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
** step was bypassed because a Bloom filter returned not-found. The ** step was bypassed because a Bloom filter returned not-found. The
** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of ** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of
** times that the Bloom filter returned a find, and thus the join step ** times that the Bloom filter returned a find, and thus the join step
** had to be processed as normal. ** had to be processed as normal.</dd>
** **
** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt> ** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt>
** <dd>^This is the approximate number of bytes of heap memory ** <dd>^This is the approximate number of bytes of heap memory
@@ -9200,9 +9200,9 @@ struct sqlite3_pcache_page {
** SQLite will typically create one cache instance for each open database file, ** SQLite will typically create one cache instance for each open database file,
** though this is not guaranteed. ^The ** though this is not guaranteed. ^The
** first parameter, szPage, is the size in bytes of the pages that must ** first parameter, szPage, is the size in bytes of the pages that must
** be allocated by the cache. ^szPage will always a power of two. ^The ** be allocated by the cache. ^szPage will always be a power of two. ^The
** second parameter szExtra is a number of bytes of extra storage ** second parameter szExtra is a number of bytes of extra storage
** associated with each page cache entry. ^The szExtra parameter will ** associated with each page cache entry. ^The szExtra parameter will be
** a number less than 250. SQLite will use the ** a number less than 250. SQLite will use the
** extra szExtra bytes on each page to store metadata about the underlying ** extra szExtra bytes on each page to store metadata about the underlying
** database page on disk. The value passed into szExtra depends ** database page on disk. The value passed into szExtra depends
@@ -9210,17 +9210,17 @@ struct sqlite3_pcache_page {
** ^The third argument to xCreate(), bPurgeable, is true if the cache being ** ^The third argument to xCreate(), bPurgeable, is true if the cache being
** created will be used to cache database pages of a file stored on disk, or ** created will be used to cache database pages of a file stored on disk, or
** false if it is used for an in-memory database. The cache implementation ** false if it is used for an in-memory database. The cache implementation
** does not have to do anything special based with the value of bPurgeable; ** does not have to do anything special based upon the value of bPurgeable;
** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will ** it is purely advisory. ^On a cache where bPurgeable is false, SQLite will
** never invoke xUnpin() except to deliberately delete a page. ** never invoke xUnpin() except to deliberately delete a page.
** ^In other words, calls to xUnpin() on a cache with bPurgeable set to ** ^In other words, calls to xUnpin() on a cache with bPurgeable set to
** false will always have the "discard" flag set to true. ** false will always have the "discard" flag set to true.
** ^Hence, a cache created with bPurgeable false will ** ^Hence, a cache created with bPurgeable set to false will
** never contain any unpinned pages. ** never contain any unpinned pages.
** **
** [[the xCachesize() page cache method]] ** [[the xCachesize() page cache method]]
** ^(The xCachesize() method may be called at any time by SQLite to set the ** ^(The xCachesize() method may be called at any time by SQLite to set the
** suggested maximum cache-size (number of pages stored by) the cache ** suggested maximum cache-size (number of pages stored) for the cache
** instance passed as the first argument. This is the value configured using ** instance passed as the first argument. This is the value configured using
** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable ** the SQLite "[PRAGMA cache_size]" command.)^ As with the bPurgeable
** parameter, the implementation is not required to do anything with this ** parameter, the implementation is not required to do anything with this
@@ -9247,12 +9247,12 @@ struct sqlite3_pcache_page {
** implementation must return a pointer to the page buffer with its content ** implementation must return a pointer to the page buffer with its content
** intact. If the requested page is not already in the cache, then the ** intact. If the requested page is not already in the cache, then the
** cache implementation should use the value of the createFlag ** cache implementation should use the value of the createFlag
** parameter to help it determined what action to take: ** parameter to help it determine what action to take:
** **
** <table border=1 width=85% align=center> ** <table border=1 width=85% align=center>
** <tr><th> createFlag <th> Behavior when page is not already in cache ** <tr><th> createFlag <th> Behavior when page is not already in cache
** <tr><td> 0 <td> Do not allocate a new page. Return NULL. ** <tr><td> 0 <td> Do not allocate a new page. Return NULL.
** <tr><td> 1 <td> Allocate a new page if it easy and convenient to do so. ** <tr><td> 1 <td> Allocate a new page if it is easy and convenient to do so.
** Otherwise return NULL. ** Otherwise return NULL.
** <tr><td> 2 <td> Make every effort to allocate a new page. Only return ** <tr><td> 2 <td> Make every effort to allocate a new page. Only return
** NULL if allocating a new page is effectively impossible. ** NULL if allocating a new page is effectively impossible.
@@ -9269,7 +9269,7 @@ struct sqlite3_pcache_page {
** as its second argument. If the third parameter, discard, is non-zero, ** as its second argument. If the third parameter, discard, is non-zero,
** then the page must be evicted from the cache. ** then the page must be evicted from the cache.
** ^If the discard parameter is ** ^If the discard parameter is
** zero, then the page may be discarded or retained at the discretion of ** zero, then the page may be discarded or retained at the discretion of the
** page cache implementation. ^The page cache implementation ** page cache implementation. ^The page cache implementation
** may choose to evict unpinned pages at any time. ** may choose to evict unpinned pages at any time.
** **
@@ -9287,7 +9287,7 @@ struct sqlite3_pcache_page {
** When SQLite calls the xTruncate() method, the cache must discard all ** When SQLite calls the xTruncate() method, the cache must discard all
** existing cache entries with page numbers (keys) greater than or equal ** existing cache entries with page numbers (keys) greater than or equal
** to the value of the iLimit parameter passed to xTruncate(). If any ** to the value of the iLimit parameter passed to xTruncate(). If any
** of these pages are pinned, they are implicitly unpinned, meaning that ** of these pages are pinned, they become implicitly unpinned, meaning that
** they can be safely discarded. ** they can be safely discarded.
** **
** [[the xDestroy() page cache method]] ** [[the xDestroy() page cache method]]
@@ -9586,7 +9586,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** application receives an SQLITE_LOCKED error, it may call the ** application receives an SQLITE_LOCKED error, it may call the
** sqlite3_unlock_notify() method with the blocked connection handle as ** sqlite3_unlock_notify() method with the blocked connection handle as
** the first argument to register for a callback that will be invoked ** the first argument to register for a callback that will be invoked
** when the blocking connections current transaction is concluded. ^The ** when the blocking connection's current transaction is concluded. ^The
** callback is invoked from within the [sqlite3_step] or [sqlite3_close] ** callback is invoked from within the [sqlite3_step] or [sqlite3_close]
** call that concludes the blocking connection's transaction. ** call that concludes the blocking connection's transaction.
** **
@@ -9606,7 +9606,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
** blocked connection already has a registered unlock-notify callback, ** blocked connection already has a registered unlock-notify callback,
** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is ** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is
** called with a NULL pointer as its second argument, then any existing ** called with a NULL pointer as its second argument, then any existing
** unlock-notify callback is canceled. ^The blocked connections ** unlock-notify callback is canceled. ^The blocked connection's
** unlock-notify callback may also be canceled by closing the blocked ** unlock-notify callback may also be canceled by closing the blocked
** connection using [sqlite3_close()]. ** connection using [sqlite3_close()].
** **
@@ -10004,7 +10004,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** support constraints. In this configuration (which is the default) if ** support constraints. In this configuration (which is the default) if
** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire ** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire
** statement is rolled back as if [ON CONFLICT | OR ABORT] had been ** statement is rolled back as if [ON CONFLICT | OR ABORT] had been
** specified as part of the users SQL statement, regardless of the actual ** specified as part of the user's SQL statement, regardless of the actual
** ON CONFLICT mode specified. ** ON CONFLICT mode specified.
** **
** If X is non-zero, then the virtual table implementation guarantees ** If X is non-zero, then the virtual table implementation guarantees
@@ -10038,7 +10038,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt> ** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt>
** <dd>Calls of the form ** <dd>Calls of the form
** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the ** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the
** the [xConnect] or [xCreate] methods of a [virtual table] implementation ** [xConnect] or [xCreate] methods of a [virtual table] implementation
** identify that virtual table as being safe to use from within triggers ** identify that virtual table as being safe to use from within triggers
** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the ** and views. Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the
** virtual table can do no serious harm even if it is controlled by a ** virtual table can do no serious harm even if it is controlled by a
@@ -10206,7 +10206,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** </table> ** </table>
** **
** ^For the purposes of comparing virtual table output values to see if the ** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered ** values are the same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS" ** to be the same. In other words, the comparison operator is "IS"
** (or "IS NOT DISTINCT FROM") and not "==". ** (or "IS NOT DISTINCT FROM") and not "==".
** **
@@ -10216,7 +10216,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** **
** ^A virtual table implementation is always free to return rows in any order ** ^A virtual table implementation is always free to return rows in any order
** it wants, as long as the "orderByConsumed" flag is not set. ^When the ** it wants, as long as the "orderByConsumed" flag is not set. ^When the
** the "orderByConsumed" flag is unset, the query planner will add extra ** "orderByConsumed" flag is unset, the query planner will add extra
** [bytecode] to ensure that the final results returned by the SQL query are ** [bytecode] to ensure that the final results returned by the SQL query are
** ordered correctly. The use of the "orderByConsumed" flag and the ** ordered correctly. The use of the "orderByConsumed" flag and the
** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful ** sqlite3_vtab_distinct() interface is merely an optimization. ^Careful
@@ -10313,7 +10313,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** sqlite3_vtab_in_next(X,P) should be one of the parameters to the ** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
** xFilter method which invokes these routines, and specifically ** xFilter method which invokes these routines, and specifically
** a parameter that was previously selected for all-at-once IN constraint ** a parameter that was previously selected for all-at-once IN constraint
** processing use the [sqlite3_vtab_in()] interface in the ** processing using the [sqlite3_vtab_in()] interface in the
** [xBestIndex|xBestIndex method]. ^(If the X parameter is not ** [xBestIndex|xBestIndex method]. ^(If the X parameter is not
** an xFilter argument that was selected for all-at-once IN constraint ** an xFilter argument that was selected for all-at-once IN constraint
** processing, then these routines return [SQLITE_ERROR].)^ ** processing, then these routines return [SQLITE_ERROR].)^
@@ -10368,7 +10368,7 @@ SQLITE_API int sqlite3_vtab_in_next(sqlite3_value *pVal, sqlite3_value **ppOut);
** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V) ** and only if *V is set to a value. ^The sqlite3_vtab_rhs_value(P,J,V)
** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th ** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th
** constraint is not available. ^The sqlite3_vtab_rhs_value() interface ** constraint is not available. ^The sqlite3_vtab_rhs_value() interface
** can return an result code other than SQLITE_OK or SQLITE_NOTFOUND if ** can return a result code other than SQLITE_OK or SQLITE_NOTFOUND if
** something goes wrong. ** something goes wrong.
** **
** The sqlite3_vtab_rhs_value() interface is usually only successful if ** The sqlite3_vtab_rhs_value() interface is usually only successful if
@@ -10396,8 +10396,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** KEYWORDS: {conflict resolution mode} ** KEYWORDS: {conflict resolution mode}
** **
** These constants are returned by [sqlite3_vtab_on_conflict()] to ** These constants are returned by [sqlite3_vtab_on_conflict()] to
** inform a [virtual table] implementation what the [ON CONFLICT] mode ** inform a [virtual table] implementation of the [ON CONFLICT] mode
** is for the SQL statement being evaluated. ** for the SQL statement being evaluated.
** **
** Note that the [SQLITE_IGNORE] constant is also used as a potential ** Note that the [SQLITE_IGNORE] constant is also used as a potential
** return value from the [sqlite3_set_authorizer()] callback and that ** return value from the [sqlite3_set_authorizer()] callback and that
@@ -10437,39 +10437,39 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt> ** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt>
** <dd>^The "double" variable pointed to by the V parameter will be set to the ** <dd>^The "double" variable pointed to by the V parameter will be set to the
** query planner's estimate for the average number of rows output from each ** query planner's estimate for the average number of rows output from each
** iteration of the X-th loop. If the query planner's estimates was accurate, ** iteration of the X-th loop. If the query planner's estimate was accurate,
** then this value will approximate the quotient NVISIT/NLOOP and the ** then this value will approximate the quotient NVISIT/NLOOP and the
** product of this value for all prior loops with the same SELECTID will ** product of this value for all prior loops with the same SELECTID will
** be the NLOOP value for the current loop. ** be the NLOOP value for the current loop.</dd>
** **
** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt> ** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the name of the index or table ** to a zero-terminated UTF-8 string containing the name of the index or table
** used for the X-th loop. ** used for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt> ** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt>
** <dd>^The "const char *" variable pointed to by the V parameter will be set ** <dd>^The "const char *" variable pointed to by the V parameter will be set
** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN] ** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
** description for the X-th loop. ** description for the X-th loop.</dd>
** **
** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt> ** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
** <dd>^The "int" variable pointed to by the V parameter will be set to the ** <dd>^The "int" variable pointed to by the V parameter will be set to the
** id for the X-th query plan element. The id value is unique within the ** id for the X-th query plan element. The id value is unique within the
** statement. The select-id is the same value as is output in the first ** statement. The select-id is the same value as is output in the first
** column of an [EXPLAIN QUERY PLAN] query. ** column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt> ** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
** <dd>The "int" variable pointed to by the V parameter will be set to the ** <dd>The "int" variable pointed to by the V parameter will be set to the
** the id of the parent of the current query element, if applicable, or ** id of the parent of the current query element, if applicable, or
** to zero if the query element has no parent. This is the same value as ** to zero if the query element has no parent. This is the same value as
** returned in the second column of an [EXPLAIN QUERY PLAN] query. ** returned in the second column of an [EXPLAIN QUERY PLAN] query.</dd>
** **
** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt> ** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
** <dd>The sqlite3_int64 output value is set to the number of cycles, ** <dd>The sqlite3_int64 output value is set to the number of cycles,
** according to the processor time-stamp counter, that elapsed while the ** according to the processor time-stamp counter, that elapsed while the
** query element was being processed. This value is not available for ** query element was being processed. This value is not available for
** all query elements - if it is unavailable the output variable is ** all query elements - if it is unavailable the output variable is
** set to -1. ** set to -1.</dd>
** </dl> ** </dl>
*/ */
#define SQLITE_SCANSTAT_NLOOP 0 #define SQLITE_SCANSTAT_NLOOP 0
@@ -10510,8 +10510,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter. ** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
** **
** Parameter "idx" identifies the specific query element to retrieve statistics ** Parameter "idx" identifies the specific query element to retrieve statistics
** for. Query elements are numbered starting from zero. A value of -1 may be ** for. Query elements are numbered starting from zero. A value of -1 may
** to query for statistics regarding the entire query. ^If idx is out of range ** retrieve statistics for the entire query. ^If idx is out of range
** - less than -1 or greater than or equal to the total number of query ** - less than -1 or greater than or equal to the total number of query
** elements used to implement the statement - a non-zero value is returned and ** elements used to implement the statement - a non-zero value is returned and
** the variable that pOut points to is unchanged. ** the variable that pOut points to is unchanged.
@@ -10668,8 +10668,8 @@ SQLITE_API int sqlite3_db_cacheflush(sqlite3*);
** triggers; and so forth. ** triggers; and so forth.
** **
** When the [sqlite3_blob_write()] API is used to update a blob column, ** When the [sqlite3_blob_write()] API is used to update a blob column,
** the pre-update hook is invoked with SQLITE_DELETE. This is because the ** the pre-update hook is invoked with SQLITE_DELETE, because
** in this case the new values are not available. In this case, when a ** the new values are not yet available. In this case, when a
** callback made with op==SQLITE_DELETE is actually a write using the ** callback made with op==SQLITE_DELETE is actually a write using the
** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns ** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns
** the index of the column being written. In other cases, where the ** the index of the column being written. In other cases, where the
@@ -10922,7 +10922,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** For an ordinary on-disk database file, the serialization is just a ** For an ordinary on-disk database file, the serialization is just a
** copy of the disk file. For an in-memory database or a "TEMP" database, ** copy of the disk file. For an in-memory database or a "TEMP" database,
** the serialization is the same sequence of bytes which would be written ** the serialization is the same sequence of bytes which would be written
** to disk if that database where backed up to disk. ** to disk if that database were backed up to disk.
** **
** The usual case is that sqlite3_serialize() copies the serialization of ** The usual case is that sqlite3_serialize() copies the serialization of
** the database into memory obtained from [sqlite3_malloc64()] and returns ** the database into memory obtained from [sqlite3_malloc64()] and returns
@@ -10931,7 +10931,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations ** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations
** are made, and the sqlite3_serialize() function will return a pointer ** are made, and the sqlite3_serialize() function will return a pointer
** to the contiguous memory representation of the database that SQLite ** to the contiguous memory representation of the database that SQLite
** is currently using for that database, or NULL if the no such contiguous ** is currently using for that database, or NULL if no such contiguous
** memory representation of the database exists. A contiguous memory ** memory representation of the database exists. A contiguous memory
** representation of the database will usually only exist if there has ** representation of the database will usually only exist if there has
** been a prior call to [sqlite3_deserialize(D,S,...)] with the same ** been a prior call to [sqlite3_deserialize(D,S,...)] with the same
@@ -11002,7 +11002,7 @@ SQLITE_API unsigned char *sqlite3_serialize(
** database is currently in a read transaction or is involved in a backup ** database is currently in a read transaction or is involved in a backup
** operation. ** operation.
** **
** It is not possible to deserialized into the TEMP database. If the ** It is not possible to deserialize into the TEMP database. If the
** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the ** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the
** function returns SQLITE_ERROR. ** function returns SQLITE_ERROR.
** **
@@ -11024,7 +11024,7 @@ SQLITE_API int sqlite3_deserialize(
sqlite3 *db, /* The database connection */ sqlite3 *db, /* The database connection */
const char *zSchema, /* Which DB to reopen with the deserialization */ const char *zSchema, /* Which DB to reopen with the deserialization */
unsigned char *pData, /* The serialized database content */ unsigned char *pData, /* The serialized database content */
sqlite3_int64 szDb, /* Number bytes in the deserialization */ sqlite3_int64 szDb, /* Number of bytes in the deserialization */
sqlite3_int64 szBuf, /* Total size of buffer pData[] */ sqlite3_int64 szBuf, /* Total size of buffer pData[] */
unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */ unsigned mFlags /* Zero or more SQLITE_DESERIALIZE_* flags */
); );
@@ -11032,7 +11032,7 @@ SQLITE_API int sqlite3_deserialize(
/* /*
** CAPI3REF: Flags for sqlite3_deserialize() ** CAPI3REF: Flags for sqlite3_deserialize()
** **
** The following are allowed values for 6th argument (the F argument) to ** The following are allowed values for the 6th argument (the F argument) to
** the [sqlite3_deserialize(D,S,P,N,M,F)] interface. ** the [sqlite3_deserialize(D,S,P,N,M,F)] interface.
** **
** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization ** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1750622754, "lastModified": 1756217674,
"narHash": "sha256-kMhs+YzV4vPGfuTpD3mwzibWUE6jotw5Al2wczI0Pv8=", "narHash": "sha256-TH1SfSP523QI7kcPiNtMAEuwZR3Jdz0MCDXPs7TS8uo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c7ab75210cb8cb16ddd8f290755d9558edde7ee1", "rev": "4e7667a90c167f7a81d906e5a75cba4ad8bee620",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -0,0 +1,14 @@
* Added an option to stay connected to a handful of peers.
* Load more messages at a time.
* Fix a set of Android not responding errors.
* Target Android 15 (API level 35) to meet new requirements.
* Support JS-less webapps.
* Fix unnecessary tunnel disconnects.
* Many small user interface tweaks.
* Update:
* CodeMirror
* OpenSSL 3.5.1
* lit 3.3.1
* picohttpparser
* speedscope 1.23.0
* sqlite 3.50.4

View File

@@ -0,0 +1,9 @@
* Private messages interface overhaul in progress.
* Added a loading indicator.
* Documented the core JavaScript.
* Fixed @-completion.
* Covered up launch on Android with the splash screen.
* Update:
* CodeMirror
* OpenSSL 3.5.2
* speedscope 1.23.1

1
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"license": "MIT",
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },

View File

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

View File

@@ -19,12 +19,12 @@ import android.os.RemoteException;
import android.os.StrictMode; import android.os.StrictMode;
import android.text.InputType; import android.text.InputType;
import android.util.Base64; import android.util.Base64;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.LayoutParams;
import android.view.Window; import android.view.Window;
import android.view.ViewTreeObserver;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.DownloadListener; import android.webkit.DownloadListener;
import android.webkit.JsPromptResult; import android.webkit.JsPromptResult;
@@ -43,6 +43,7 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class TildeFriendsActivity extends Activity { public class TildeFriendsActivity extends Activity {
@@ -50,39 +51,44 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsWebView web_view; TildeFriendsWebView web_view;
String base_url; String base_url;
String port_file_path; String port_file_path;
Thread create_thread;
Thread server_thread; Thread server_thread;
Thread log_thread;
ServiceConnection service_connection; ServiceConnection service_connection;
FileObserver observer; FileObserver observer;
LinkedBlockingQueue<String> log_queue = new LinkedBlockingQueue<String>();
private ValueCallback<Uri[]> upload_message; private ValueCallback<Uri[]> upload_message;
private final static int FILECHOOSER_RESULT = 1; private final static int FILECHOOSER_RESULT = 1;
private float touch_down_y; private float touch_down_y;
private boolean ready = false;
private boolean loaded = false;
private boolean shutting_down = false;
static { static {
Log.w("tildefriends", "Calling system.loadLibrary()."); log("Calling system.loadLibrary().");
System.loadLibrary("tildefriends"); System.loadLibrary("tildefriends");
Log.w("tildefriends", "system.loadLibrary() completed."); log("system.loadLibrary() completed.");
} }
public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path, ConnectivityManager connectivity_manager); public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path, ConnectivityManager connectivity_manager);
public static native int tf_sandbox_main(int pipe_fd); public static native int tf_sandbox_main(int pipe_fd);
@Override public static void log(String message) {
protected void onCreate(Bundle savedInstanceState) { if (s_activity != null && s_activity.log_queue != null && message != null) {
s_activity = this; try {
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() s_activity.log_queue.put(message);
.detectLeakedClosableObjects() } catch (InterruptedException e) {
.penaltyLog() android.util.Log.w("tildefriends", message);
.build()); }
super.onCreate(savedInstanceState); }
this.requestWindowFeature(Window.FEATURE_NO_TITLE); }
setContentView(R.layout.activity_main);
private void createThread() {
web_view = (TildeFriendsWebView)findViewById(R.id.web); web_view = (TildeFriendsWebView)findViewById(R.id.web);
set_status("Extracting executable..."); log(String.format("getFilesDir() is %s", getFilesDir().toString()));
Log.w("tildefriends", String.format("getFilesDir() is %s", getFilesDir().toString())); log(String.format("getPackageResourcePath() is %s", getPackageResourcePath().toString()));
Log.w("tildefriends", String.format("getPackageResourcePath() is %s", getPackageResourcePath().toString())); log(String.format("nativeLibraryDir is %s", getApplicationInfo().nativeLibraryDir));
Log.w("tildefriends", String.format("nativeLibraryDir is %s", getApplicationInfo().nativeLibraryDir));
port_file_path = getFilesDir().toString() + "/port.txt"; port_file_path = getFilesDir().toString() + "/port.txt";
new File(port_file_path).delete(); new File(port_file_path).delete();
@@ -90,178 +96,256 @@ public class TildeFriendsActivity extends Activity {
TildeFriendsActivity activity = this; TildeFriendsActivity activity = this;
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() { server_thread = new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Log.w("tildefriends", "Calling tf_server_main."); log("Watching for changes in: " + getFilesDir().toString());
observer = make_file_observer(getFilesDir().toString(), port_file_path);
observer.startWatching();
log("Calling tf_server_main.");
int result = tf_server_main( int result = tf_server_main(
getFilesDir().toString(), getFilesDir().toString(),
getPackageResourcePath().toString(), getPackageResourcePath().toString(),
port_file_path, port_file_path,
(ConnectivityManager)getApplicationContext().getSystemService(CONNECTIVITY_SERVICE)); (ConnectivityManager)getApplicationContext().getSystemService(CONNECTIVITY_SERVICE));
Log.w("tildefriends", "tf_server_main returned " + result + "."); log("tf_server_main returned " + result + ".");
} }
}); });
server_thread.start(); server_thread.start();
web_view.getSettings().setJavaScriptEnabled(true); runOnUiThread(() -> {
web_view.getSettings().setDatabaseEnabled(true); web_view.getSettings().setJavaScriptEnabled(true);
web_view.getSettings().setDomStorageEnabled(true); web_view.getSettings().setDomStorageEnabled(true);
set_database_path(); set_database_enabled();
set_database_path();
web_view.setDownloadListener(new DownloadListener() { web_view.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String content_disposition, String mime_type, long content_length) { 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 + ")"); log("Let's download: " + url + " (" + content_disposition + ")");
String file_name = URLUtil.guessFileName(url, content_disposition, mime_type); String file_name = URLUtil.guessFileName(url, content_disposition, mime_type);
if (url.startsWith("data:") && url.indexOf(',') != -1) { if (url.startsWith("data:") && url.indexOf(',') != -1) {
String b64 = url.substring(url.indexOf(',') + 1); String b64 = url.substring(url.indexOf(',') + 1);
byte[] data = Base64.decode(b64, Base64.DEFAULT); byte[] data = Base64.decode(b64, Base64.DEFAULT);
Log.w("tildefriends", "Downloaded " + data.length + " bytes."); log("Downloaded " + data.length + " bytes.");
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
try (OutputStream stream = new FileOutputStream(new File(path, file_name))) { try (OutputStream stream = new FileOutputStream(new File(path, file_name))) {
stream.write(data); stream.write(data);
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e.toString()); log("IOException: " + e.toString());
}
Toast.makeText(getApplicationContext(), "Downloaded File", Toast.LENGTH_LONG).show();
} else {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setMimeType(mime_type);
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Downloading file...");
request.setTitle(file_name);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, URLUtil.guessFileName(url, content_disposition, mime_type));
DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
Toast.makeText(getApplicationContext(), "Downloading File", Toast.LENGTH_LONG).show();
} }
Toast.makeText(getApplicationContext(), "Downloaded File", Toast.LENGTH_LONG).show();
} else {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setMimeType(mime_type);
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Downloading file...");
request.setTitle(file_name);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, URLUtil.guessFileName(url, content_disposition, mime_type));
DownloadManager dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
Toast.makeText(getApplicationContext(), "Downloading File", Toast.LENGTH_LONG).show();
} }
} });
});
web_view.setWebChromeClient(new WebChromeClient() { web_view.setWebChromeClient(new WebChromeClient() {
@Override @Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) { public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext()) new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends") .setTitle("Tilde Friends")
.setMessage(message) .setMessage(message)
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{ {
result.confirm(); public void onClick(DialogInterface dialog, int which)
} {
}) result.confirm();
.create() }
.show(); })
return true; .create()
} .show();
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(defaultValue);
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
/*
** https://stackoverflow.com/questions/5907369/file-upload-in-webview
** https://stackoverflow.com/questions/8586691/how-to-open-file-save-dialog-in-android
*/
@Override
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> message, WebChromeClient.FileChooserParams params) {
upload_message = message;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
TildeFriendsActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), TildeFriendsActivity.FILECHOOSER_RESULT);
return true;
}
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
Log.d("tildefriends", consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
}
});
web_view.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
{
if (request.getUrl() != null && request.getUrl().toString().startsWith(base_url)) {
return false;
} else {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, request.getUrl()));
return true; return true;
} }
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
EditText input = new EditText(view.getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(defaultValue);
new AlertDialog.Builder(view.getContext())
.setTitle("Tilde Friends")
.setMessage(message)
.setView(input)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.confirm(input.getText().toString());
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
{
public void onClick(DialogInterface dialog, int which)
{
result.cancel();
}
})
.create()
.show();
return true;
}
/*
** https://stackoverflow.com/questions/5907369/file-upload-in-webview
** https://stackoverflow.com/questions/8586691/how-to-open-file-save-dialog-in-android
*/
@Override
public boolean onShowFileChooser(WebView view, ValueCallback<Uri[]> message, WebChromeClient.FileChooserParams params) {
upload_message = message;
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
TildeFriendsActivity.this.startActivityForResult(Intent.createChooser(intent, "File Chooser"), TildeFriendsActivity.FILECHOOSER_RESULT);
return true;
}
@Override
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
log(consoleMessage.message() + " -- From line " + consoleMessage.lineNumber() + " of " + consoleMessage.sourceId());
return true;
}
});
web_view.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
{
if (request.getUrl() != null && request.getUrl().toString().startsWith(base_url)) {
return false;
} else {
view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, request.getUrl()));
return true;
}
}
@Override
public void onPageFinished(WebView view, String url) {
s_activity.loaded = true;
}
});
}); });
s_activity.create_thread = null;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
super.onCreate(savedInstanceState);
log_thread = new Thread(new Runnable() {
@Override
public void run() {
while (!s_activity.shutting_down) {
try {
String message = log_queue.take();
if (message != null) {
android.util.Log.w("tildefriends", message);
} else {
break;
}
} catch (InterruptedException e) {
}
}
}
});
log_thread.start();
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDialog()
.penaltyLog()
.build());
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.detectAll()
.penaltyLog()
.build());
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
TextView refresh = (TextView)findViewById(R.id.refresh); TextView refresh = (TextView)findViewById(R.id.refresh);
refresh.setVisibility(View.GONE); refresh.setVisibility(View.GONE);
refresh.setText("REFRESH"); refresh.setText("REFRESH");
final View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (s_activity.ready && s_activity.loaded) {
content.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
} else {
return false;
}
}
}
);
create_thread = new Thread(new Runnable() {
@Override
public void run() {
s_activity.createThread();
}
});
create_thread.start();
} }
@Override @Override
protected void onDestroy() protected void onDestroy()
{ {
super.onDestroy(); try {
shutting_down = true;
if (log_queue != null) {
log_queue.put("Goodbye.");
}
log_thread.join();
} catch (InterruptedException e) {
}
log_thread = null;
s_activity = null; s_activity = null;
super.onDestroy();
} }
@Override @Override
@@ -353,46 +437,33 @@ public class TildeFriendsActivity extends Activity {
return -1; return -1;
} }
private void set_status(String text) {
TextView text_view = (TextView)findViewById(R.id.text);
web_view.setVisibility(View.GONE);
text_view.setVisibility(View.VISIBLE);
text_view.setText(text);
}
private void hide_status() {
TextView text_view = (TextView)findViewById(R.id.text);
web_view.setVisibility(View.VISIBLE);
text_view.setVisibility(View.GONE);
}
public static void start_sandbox(int pipe_fd) { public static void start_sandbox(int pipe_fd) {
Log.w("tildefriends", "starting service with fd: " + pipe_fd); log("starting service with fd: " + pipe_fd);
Intent intent = new Intent(s_activity, TildeFriendsSandboxService.class); Intent intent = new Intent(s_activity, TildeFriendsSandboxService.class);
s_activity.service_connection = new ServiceConnection() { s_activity.service_connection = new ServiceConnection() {
@Override @Override
public void onBindingDied(ComponentName name) { public void onBindingDied(ComponentName name) {
Log.w("tildefriends", "onBindingDied"); log("onBindingDied");
} }
@Override @Override
public void onNullBinding(ComponentName name) { public void onNullBinding(ComponentName name) {
Log.w("tildefriends", "onNullBinding"); log("onNullBinding");
} }
@Override @Override
public void onServiceConnected(ComponentName name, IBinder binder) { public void onServiceConnected(ComponentName name, IBinder binder) {
Log.w("tildefriends", "onServiceConnected"); log("onServiceConnected");
Parcel data = Parcel.obtain(); Parcel data = Parcel.obtain();
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(pipe_fd)) { try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(pipe_fd)) {
data.writeParcelable(pfd, 0); data.writeParcelable(pfd, 0);
} catch (java.io.IOException e) { } catch (java.io.IOException e) {
Log.w("tildefriends", "IOException: " + e); log("IOException: " + e);
} }
try { try {
binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY); binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w("tildefriends", "RemoteException"); log("RemoteException");
} finally { } finally {
data.recycle(); data.recycle();
} }
@@ -400,14 +471,14 @@ public class TildeFriendsActivity extends Activity {
@Override @Override
public void onServiceDisconnected(ComponentName name) { public void onServiceDisconnected(ComponentName name) {
Log.w("tildefriends", "onServiceDisconnected"); log("onServiceDisconnected");
} }
}; };
s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE); s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND);
} }
public static void stop_sandbox() { public static void stop_sandbox() {
Log.w("tildefriends", "stop_sandbox"); log("stop_sandbox");
if (s_activity.service_connection != null) { if (s_activity.service_connection != null) {
s_activity.unbindService(s_activity.service_connection); s_activity.unbindService(s_activity.service_connection);
s_activity.service_connection = null; s_activity.service_connection = null;
@@ -419,14 +490,11 @@ public class TildeFriendsActivity extends Activity {
if (port >= 0) { if (port >= 0) {
base_url = "http://127.0.0.1:" + String.valueOf(port) + "/"; base_url = "http://127.0.0.1:" + String.valueOf(port) + "/";
runOnUiThread(() -> { runOnUiThread(() -> {
hide_status(); ready = true;
web_view.loadUrl(base_url + "login/auto"); web_view.loadUrl(base_url + "login/auto");
}); });
observer.stopWatching();
observer = null; observer = null;
} else {
runOnUiThread(() -> {
set_status("Waiting to connect...");
});
} }
} }
@@ -451,4 +519,10 @@ public class TildeFriendsActivity extends Activity {
web_view.getSettings().setDatabasePath(getDatabasePath("webview").getPath()); web_view.getSettings().setDatabasePath(getDatabasePath("webview").getPath());
} }
} }
@SuppressWarnings("deprecation")
private void set_database_enabled()
{
web_view.getSettings().setDatabaseEnabled(true);
}
} }

View File

@@ -6,7 +6,6 @@ import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.os.Parcel; import android.os.Parcel;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.util.Log;
public class TildeFriendsSandboxService extends Service { public class TildeFriendsSandboxService extends Service {
public static final int START_CALL = IBinder.FIRST_CALL_TRANSACTION; public static final int START_CALL = IBinder.FIRST_CALL_TRANSACTION;
@@ -14,12 +13,12 @@ public class TildeFriendsSandboxService extends Service {
Thread thread; Thread thread;
public int onStartCommand(Intent intent, int flags, int start_id) { public int onStartCommand(Intent intent, int flags, int start_id) {
Log.w("tildefriends", "TildeFriendsSandboxService: onStartCommand"); TildeFriendsActivity.log("TildeFriendsSandboxService: onStartCommand");
return super.onStartCommand(intent, flags, start_id); return super.onStartCommand(intent, flags, start_id);
} }
public void onDestroy() { public void onDestroy() {
Log.w("tildefriends", "TildeFriendsSandboxService: onDestroy"); TildeFriendsActivity.log("TildeFriendsSandboxService: onDestroy");
super.onDestroy(); super.onDestroy();
} }
@@ -27,9 +26,9 @@ public class TildeFriendsSandboxService extends Service {
thread = new Thread(new Runnable() { thread = new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Log.w("tildefriends", "Calling tf_sandbox_main."); TildeFriendsActivity.log("Calling tf_sandbox_main.");
int result = TildeFriendsActivity.tf_sandbox_main(pipe_fd); int result = TildeFriendsActivity.tf_sandbox_main(pipe_fd);
Log.w("tildefriends", "tf_sandbox_main returned " + result + "."); TildeFriendsActivity.log("tf_sandbox_main returned " + result + ".");
} }
}); });
thread.start(); thread.start();
@@ -43,7 +42,7 @@ public class TildeFriendsSandboxService extends Service {
if (code == START_CALL) { if (code == START_CALL) {
ParcelFileDescriptor pfd = read_pfd(data); ParcelFileDescriptor pfd = read_pfd(data);
if (pfd != null) { if (pfd != null) {
Log.w("tildefriends", "fd is " + pfd.getFd()); TildeFriendsActivity.log("fd is " + pfd.getFd());
start_thread(pfd.detachFd()); start_thread(pfd.detachFd());
try { try {
pfd.close(); pfd.close();

View File

@@ -3,16 +3,13 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
android:fitsSystemWindows="true"
android:background="#000">
<com.unprompted.tildefriends.TildeFriendsWebView <com.unprompted.tildefriends.TildeFriendsWebView
android:id="@+id/web" android:id="@+id/web"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal|center_vertical"/>
<TextView <TextView
android:id="@+id/refresh" android:id="@+id/refresh"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -1,11 +1,225 @@
#include "api.js.h" #include "api.js.h"
#include "log.h" #include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include <quickjs.h> #include <quickjs.h>
typedef struct _app_path_pair_t
{
const char* app;
const char* path;
} app_path_pair_t;
typedef struct _get_apps_t
{
app_path_pair_t* apps;
int count;
JSContext* context;
JSValue promise[2];
char user[];
} get_apps_t;
static void _tf_api_core_apps_work(tf_ssb_t* ssb, void* user_data)
{
get_apps_t* work = user_data;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* apps = tf_ssb_db_get_property(ssb, work->user, "apps");
if (apps)
{
JSValue apps_array = JS_ParseJSON(context, apps, strlen(apps), NULL);
if (JS_IsArray(context, apps_array))
{
int length = tf_util_get_length(context, apps_array);
for (int i = 0; i < length; i++)
{
JSValue name = JS_GetPropertyUint32(context, apps_array, i);
const char* name_string = JS_ToCString(context, name);
if (name_string)
{
work->apps = tf_resize_vec(work->apps, sizeof(app_path_pair_t) * (work->count + 1));
work->apps[work->count].app = tf_strdup(name_string);
size_t size = strlen("path:") + strlen(name_string) + 1;
char* path_key = tf_malloc(size);
snprintf(path_key, size, "path:%s", name_string);
work->apps[work->count].path = tf_ssb_db_get_property(ssb, work->user, path_key);
tf_free(path_key);
work->count++;
}
JS_FreeCString(context, name_string);
JS_FreeValue(context, name);
}
}
JS_FreeValue(context, apps_array);
}
tf_free((void*)apps);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _tf_api_core_apps_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
get_apps_t* work = user_data;
JSContext* context = work->context;
JSValue result = JS_NewObject(context);
for (int i = 0; i < work->count; i++)
{
JS_SetPropertyStr(context, result, work->apps[i].app, JS_NewString(context, work->apps[i].path));
}
JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result);
tf_util_report_error(context, error);
JS_FreeValue(context, error);
JS_FreeValue(context, result);
JS_FreeValue(context, work->promise[0]);
JS_FreeValue(context, work->promise[1]);
for (int i = 0; i < work->count; i++)
{
tf_free((void*)work->apps[i].app);
tf_free((void*)work->apps[i].path);
}
tf_free(work->apps);
tf_free(work);
}
static JSValue _tf_api_core_apps(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue result = JS_UNDEFINED;
JSValue user = argv[0];
JSValue process = data[0];
const char* user_string = JS_IsString(user) ? JS_ToCString(context, user) : NULL;
if (JS_IsObject(process))
{
JSValue credentials = JS_GetPropertyStr(context, process, "credentials");
if (JS_IsObject(credentials))
{
JSValue session = JS_GetPropertyStr(context, credentials, "session");
if (JS_IsObject(session))
{
JSValue session_name = JS_GetPropertyStr(context, session, "name");
const char* session_name_string = JS_IsString(session_name) ? JS_ToCString(context, session_name) : NULL;
if (user_string && session_name_string && strcmp(user_string, session_name_string) && strcmp(user_string, "core"))
{
JS_FreeCString(context, user_string);
user_string = NULL;
}
else if (!user_string)
{
user_string = session_name_string;
session_name_string = NULL;
}
JS_FreeCString(context, session_name_string);
JS_FreeValue(context, session_name);
}
JS_FreeValue(context, session);
}
JS_FreeValue(context, credentials);
}
if (user_string)
{
get_apps_t* work = tf_malloc(sizeof(get_apps_t) + strlen(user_string) + 1);
*work = (get_apps_t) {
.context = context,
};
memcpy(work->user, user_string, strlen(user_string) + 1);
result = JS_NewPromiseCapability(context, work->promise);
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_ssb_run_work(ssb, _tf_api_core_apps_work, _tf_api_core_apps_after_work, work);
}
else
{
result = JS_NewObject(context);
}
JS_FreeCString(context, user_string);
return result;
}
static JSValue _tf_api_core_register(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue event_name = argv[0];
JSValue handler = argv[1];
JSValue process = data[0];
JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
JSAtom atom = JS_ValueToAtom(context, event_name);
JSValue array = JS_GetProperty(context, event_handlers, atom);
if (!JS_IsArray(context, array))
{
JS_FreeValue(context, array);
array = JS_NewArray(context);
JS_SetProperty(context, event_handlers, atom, JS_DupValue(context, array));
}
JS_SetPropertyUint32(context, array, tf_util_get_length(context, array), JS_DupValue(context, handler));
JS_FreeValue(context, array);
JS_FreeAtom(context, atom);
JS_FreeValue(context, event_handlers);
return JS_UNDEFINED;
}
static JSValue _tf_api_core_unregister(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data)
{
JSValue event_name = argv[0];
JSValue handler = argv[1];
JSValue process = data[0];
JSValue event_handlers = JS_GetPropertyStr(context, process, "eventHandlers");
JSAtom atom = JS_ValueToAtom(context, event_name);
JSValue array = JS_GetProperty(context, event_handlers, atom);
if (JS_IsArray(context, array))
{
JSValue index_of = JS_GetPropertyStr(context, array, "indexOf");
JSValue index = JS_Call(context, index_of, array, 1, &handler);
int int_index = -1;
JS_ToInt32(context, &int_index, index);
if (int_index != -1)
{
JSValue splice = JS_GetPropertyStr(context, array, "splice");
JSValue splice_args[] =
{
index,
JS_NewInt32(context, 1),
};
JSValue result = JS_Call(context, splice, array, 2, splice_args);
JS_FreeValue(context, result);
JS_FreeValue(context, splice);
}
JS_FreeValue(context, index);
JS_FreeValue(context, index_of);
if (tf_util_get_length(context, array) == 0)
{
JS_DeleteProperty(context, event_handlers, atom, 0);
}
}
JS_FreeValue(context, array);
JS_FreeAtom(context, atom);
JS_FreeValue(context, event_handlers);
return JS_UNDEFINED;
}
static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) static JSValue _tf_api_register_imports(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{ {
JSValue imports = argv[0];
JSValue process = argv[1];
JSValue core = JS_GetPropertyStr(context, imports, "core");
JS_SetPropertyStr(context, core, "apps", JS_NewCFunctionData(context, _tf_api_core_apps, 1, 0, 1, &process));
JS_SetPropertyStr(context, core, "register", JS_NewCFunctionData(context, _tf_api_core_register, 2, 0, 1, &process));
JS_SetPropertyStr(context, core, "unregister", JS_NewCFunctionData(context, _tf_api_core_unregister, 2, 0, 1, &process));
JS_FreeValue(context, core);
return JS_UNDEFINED; return JS_UNDEFINED;
} }

252
src/httpd.app.c Normal file
View File

@@ -0,0 +1,252 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include "picohttpparser.h"
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _app_blob_t
{
tf_http_request_t* request;
bool found;
bool not_modified;
bool use_handler;
bool use_static;
void* data;
size_t size;
char app_blob_id[k_blob_id_len];
const char* file;
tf_httpd_user_app_t* user_app;
char etag[256];
} app_blob_t;
static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data)
{
app_blob_t* data = user_data;
tf_http_request_t* request = data->request;
if (request->path[0] == '/' && request->path[1] == '~')
{
const char* last_slash = strchr(request->path + 1, '/');
if (last_slash)
{
last_slash = strchr(last_slash + 1, '/');
}
data->user_app = last_slash ? tf_httpd_parse_user_app_from_path(request->path, last_slash) : NULL;
if (data->user_app)
{
size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1;
char* app_path = tf_malloc(path_length);
snprintf(app_path, path_length, "path:%s", data->user_app->app);
const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path);
tf_string_set(data->app_blob_id, sizeof(data->app_blob_id), value);
tf_free(app_path);
tf_free((void*)value);
data->file = last_slash + 1;
}
}
else if (request->path[0] == '/' && request->path[1] == '&')
{
const char* end = strstr(request->path, ".sha256/");
if (end)
{
snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1);
data->file = end + strlen(".sha256/");
}
}
char* app_blob = NULL;
size_t app_blob_size = 0;
if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size))
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL);
JSValue files = JS_GetPropertyStr(context, app_object, "files");
JSValue blob_id = JS_GetPropertyStr(context, files, data->file);
if (JS_IsUndefined(blob_id))
{
blob_id = JS_GetPropertyStr(context, files, "handler.js");
if (!JS_IsUndefined(blob_id))
{
data->use_handler = true;
}
}
else
{
const char* blob_id_str = JS_ToCString(context, blob_id);
if (blob_id_str)
{
snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str);
const char* match = tf_http_request_get_header(data->request, "if-none-match");
if (match && strcmp(match, data->etag) == 0)
{
data->not_modified = true;
}
else
{
data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size);
}
}
JS_FreeCString(context, blob_id_str);
}
JS_FreeValue(context, blob_id);
JS_FreeValue(context, files);
JS_FreeValue(context, app_object);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
tf_free(app_blob);
}
}
static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler");
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue handler_blob_id = JS_NewString(context, app_blob_id);
JSValue path_value = JS_NewString(context, path);
JSValue package_owner_value = JS_NewString(context, package_owner);
JSValue app_value = JS_NewString(context, app);
JSValue query_value = request->query ? JS_NewString(context, request->query) : JS_UNDEFINED;
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
char name[256] = "";
snprintf(name, sizeof(name), "%.*s", (int)request->headers[i].name_len, request->headers[i].name);
JS_SetPropertyStr(context, headers, name, JS_NewStringLen(context, request->headers[i].value, request->headers[i].value_len));
}
JSValue args[] = {
response,
handler_blob_id,
path_value,
query_value,
headers,
package_owner_value,
app_value,
};
JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, headers);
JS_FreeValue(context, query_value);
JS_FreeValue(context, app_value);
JS_FreeValue(context, package_owner_value);
JS_FreeValue(context, handler_blob_id);
JS_FreeValue(context, path_value);
JS_FreeValue(context, response);
JS_FreeValue(context, call_app_handler);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}
static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_blob_t* data = user_data;
if (data->not_modified)
{
tf_http_respond(data->request, 304, NULL, 0, NULL, 0);
}
else if (data->use_static)
{
tf_httpd_endpoint_static(data->request);
}
else if (data->use_handler)
{
_httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app->user, data->user_app->app);
}
else if (data->found)
{
const char* mime_type = tf_httpd_ext_to_content_type(strrchr(data->request->path, '.'), false);
if (!mime_type)
{
mime_type = tf_httpd_magic_bytes_to_content_type(data->data, data->size);
}
const char* headers[] = {
"Access-Control-Allow-Origin",
"*",
"Content-Security-Policy",
"sandbox allow-downloads allow-top-navigation-by-user-activation",
"Content-Type",
mime_type ? mime_type : "application/binary",
"etag",
data->etag,
};
tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size);
}
tf_free(data->user_app);
tf_free(data->data);
tf_http_request_unref(data->request);
tf_free(data);
}
void tf_httpd_endpoint_app(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);
app_blob_t* data = tf_malloc(sizeof(app_blob_t));
*data = (app_blob_t) { .request = request };
tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data);
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
}
JS_SetPropertyStr(context, request_object, "headers", headers);
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
JSValue args[] = {
request_object,
response,
};
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
}

91
src/httpd.delete.c Normal file
View File

@@ -0,0 +1,91 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
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 = tf_httpd_authenticate_jwt(ssb, context, delete->session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && tf_httpd_is_name_valid(user_string))
{
tf_httpd_user_app_t* user_app = tf_httpd_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, NULL, 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);
}
void tf_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);
}

233
src/httpd.index.c Normal file
View File

@@ -0,0 +1,233 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _index_t
{
tf_http_request_t* request;
bool found;
bool not_modified;
bool use_handler;
bool use_static;
void* data;
size_t size;
char app_blob_id[k_blob_id_len];
const char* file;
tf_httpd_user_app_t* user_app;
char etag[256];
} index_t;
static bool _has_property(JSContext* context, JSValue object, const char* name)
{
JSAtom atom = JS_NewAtom(context, name);
bool result = JS_HasProperty(context, object, atom) > 0;
JS_FreeAtom(context, atom);
return result;
}
static void _httpd_endpoint_app_index_work(tf_ssb_t* ssb, void* user_data)
{
index_t* data = user_data;
data->use_static = true;
tf_httpd_user_app_t* user_app = data->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* app_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path);
tf_free(app_path);
uint8_t* app_blob = NULL;
size_t app_blob_size = 0;
if (tf_ssb_db_blob_get(ssb, app_blob_id, &app_blob, &app_blob_size))
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
JSValue app = JS_ParseJSON(context, (const char*)app_blob, app_blob_size, NULL);
JSValue files = JS_GetPropertyStr(context, app, "files");
if (!_has_property(context, files, "app.js"))
{
JSValue index = JS_GetPropertyStr(context, files, "index.html");
if (JS_IsString(index))
{
const char* index_string = JS_ToCString(context, index);
tf_ssb_db_blob_get(ssb, index_string, (uint8_t**)&data->data, &data->size);
JS_FreeCString(context, index_string);
}
JS_FreeValue(context, index);
}
JS_FreeValue(context, files);
JS_FreeValue(context, app);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
tf_free(app_blob);
}
tf_free((void*)app_blob_id);
}
static char* _replace(const char* original, size_t size, const char* find, const char* replace, size_t* out_size)
{
char* pos = strstr(original, find);
if (!pos)
{
return tf_strdup(original);
}
size_t replace_length = strlen(replace);
size_t find_length = strlen(find);
size_t new_size = size + replace_length - find_length;
char* buffer = tf_malloc(new_size);
memcpy(buffer, original, pos - original);
memcpy(buffer + (pos - original), replace, replace_length);
memcpy(buffer + (pos - original) + replace_length, pos + find_length, size - (pos - original) - find_length);
*out_size = new_size;
return buffer;
}
static char* _append_raw(char* document, size_t* current_size, const char* data, size_t size)
{
document = tf_resize_vec(document, *current_size + size);
memcpy(document + *current_size, data, size);
document[*current_size + size] = '\0';
*current_size += size;
return document;
}
static char* _append_encoded(char* document, const char* data, size_t size, size_t* out_size)
{
size_t current_size = strlen(document);
int accum = 0;
for (int i = 0; (size_t)i < size; i++)
{
switch (data[i])
{
case '"':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&quot;", strlen("&quot;"));
accum = i + 1;
break;
case '\'':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&#x27;", strlen("&#x27;"));
accum = i + 1;
break;
case '<':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&lt;", strlen("&lt;"));
accum = i + 1;
break;
case '>':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&gt;", strlen("&gt;"));
accum = i + 1;
break;
case '&':
if (i > accum)
{
document = _append_raw(document, &current_size, data + accum, i - accum);
}
document = _append_raw(document, &current_size, "&amp;", strlen("&amp;"));
accum = i + 1;
break;
default:
break;
}
}
*out_size = current_size;
return document;
}
static void _httpd_endpoint_app_index_file_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
index_t* state = user_data;
if (result > 0)
{
char* replacement = tf_strdup("<iframe srcdoc=\"");
size_t replacement_size = 0;
replacement = _append_encoded(replacement, state->data, state->size, &replacement_size);
replacement = _append_raw(replacement, &replacement_size, "\"", 1);
size_t size = 0;
char* document = _replace(data, result, "<iframe", replacement, &size);
const char* headers[] = {
"Content-Type",
"text/html; charset=utf-8",
};
tf_http_respond(state->request, 200, headers, tf_countof(headers) / 2, document, size);
tf_free(replacement);
tf_free(document);
}
tf_free(state->data);
tf_free(state->user_app);
tf_http_request_unref(state->request);
tf_free(state);
}
static void _httpd_endpoint_app_index_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
index_t* data = user_data;
if (data->data)
{
tf_task_t* task = data->request->user_data;
const char* root_path = tf_task_get_root_path(task);
size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen("core/index.html") + 1;
char* path = alloca(size);
snprintf(path, size, "%s%score/index.html", root_path ? root_path : "", root_path ? "/" : "");
tf_file_read(task, path, _httpd_endpoint_app_index_file_read, data);
}
else
{
tf_httpd_endpoint_static(data->request);
tf_free(data->user_app);
tf_http_request_unref(data->request);
tf_free(data);
}
}
void tf_httpd_endpoint_app_index(tf_http_request_t* request)
{
tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/");
if (!user_app)
{
return tf_httpd_endpoint_static(request);
}
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
index_t* data = tf_malloc(sizeof(index_t));
(*data) = (index_t) { .request = request, .user_app = user_app };
tf_http_request_ref(request);
tf_ssb_run_work(ssb, _httpd_endpoint_app_index_work, _httpd_endpoint_app_index_after_work, data);
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,14 @@
** @{ ** @{
*/ */
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "quickjs.h"
static const int64_t k_httpd_auth_refresh_interval = 1ULL * 7 * 24 * 60 * 60 * 1000;
/** A JS context. */ /** A JS context. */
typedef struct JSContext JSContext; typedef struct JSContext JSContext;
@@ -19,6 +27,27 @@ typedef struct JSContext JSContext;
*/ */
typedef struct _tf_http_t tf_http_t; typedef struct _tf_http_t tf_http_t;
/**
** An HTTP request.
*/
typedef struct _tf_http_request_t tf_http_request_t;
/**
** An SSB instance.
*/
typedef struct _tf_ssb_t tf_ssb_t;
/**
** A user and app name.
*/
typedef struct _tf_httpd_user_app_t
{
/** The username. */
const char* user;
/** The app name. */
const char* app;
} tf_httpd_user_app_t;
/** /**
** Register the HTTP script interface. Also registers a number of built-in ** Register the HTTP script interface. Also registers a number of built-in
** request handlers. An ongoing project is to move the JS request handlers ** request handlers. An ongoing project is to move the JS request handlers
@@ -39,4 +68,153 @@ tf_http_t* tf_httpd_create(JSContext* context);
*/ */
void tf_httpd_destroy(tf_http_t* http); void tf_httpd_destroy(tf_http_t* http);
/**
** Determine a content-type from a file extension.
** @param ext The file extension.
** @param use_fallback If not found, fallback to application/binary.
** @return A MIME type or NULL.
*/
const char* tf_httpd_ext_to_content_type(const char* ext, bool use_fallback);
/**
** Determine a content type from magic bytes.
** @param bytes The first bytes of a file.
** @param size The length of the bytes.
** @return A MIME type or NULL.
*/
const char* tf_httpd_magic_bytes_to_content_type(const uint8_t* bytes, size_t size);
/**
** Respond with a redirect.
** @param request The HTTP request.
** @return true if redirected.
*/
bool tf_httpd_redirect(tf_http_request_t* request);
/**
** Parse a username and app from a path like /~user/app/.
** @param path The path.
** @param expected_suffix A suffix that is required to be on the path, and removed.
** @return The user and app. Free with tf_free().
*/
tf_httpd_user_app_t* tf_httpd_parse_user_app_from_path(const char* path, const char* expected_suffix);
/**
** Decode form data into key value pairs.
** @param data The form data string.
** @param length The length of the form data string.
** @return Key values pairs terminated by NULL.
*/
const char** tf_httpd_form_data_decode(const char* data, int length);
/**
** Get a form data value from an array of key value pairs produced by tf_httpd_form_data_decode().
** @param form_data The form data.
** @param key The key for which to fetch the value.
** @return the value for the case-insensitive key or NULL.
*/
const char* tf_httpd_form_data_get(const char** form_data, const char* key);
/**
** Validate a JWT.
** @param ssb The SSB instance.
** @param context A JS context.
** @param jwt The JWT.
** @return The JWT contents if valid.
*/
JSValue tf_httpd_authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* jwt);
;
/**
** Make a JS response object for a request.
** @param context The JS context.
** @param request The HTTP request.
** @return The respone object.
*/
JSValue tf_httpd_make_response_object(JSContext* context, tf_http_request_t* request);
/**
** Check if a name meets requirements.
** @param name The name.
** @return true if the name is valid.
*/
bool tf_httpd_is_name_valid(const char* name);
/**
** Make a header for the session cookie.
** @param request The HTTP request.
** @param session_cookie The session cookie.
** @return The header.
*/
const char* tf_httpd_make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie);
/**
** Make a JWT for the session.
** @param context A JS context.
** @param ssb The SSB instance.
** @param name The username.
** @return The JWT.
*/
const char* tf_httpd_make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name);
/**
** Serve a static file.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_static(tf_http_request_t* request);
/**
** View a blob.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_view(tf_http_request_t* request);
/**
** Save a blob or app.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_save(tf_http_request_t* request);
/**
** Delete a blob or app.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_delete(tf_http_request_t* request);
/**
** App endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app(tf_http_request_t* request);
/**
** App index endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app_index(tf_http_request_t* request);
/**
** App WebSocket.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_app_socket(tf_http_request_t* request);
/**
** Login endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_login(tf_http_request_t* request);
/**
** Auto-login endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_login_auto(tf_http_request_t* request);
/**
** Logout endpoint.
** @param request The HTTP request.
*/
void tf_httpd_endpoint_logout(tf_http_request_t* request);
/** @} */ /** @} */

537
src/httpd.login.c Normal file
View File

@@ -0,0 +1,537 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
#include "ow-crypt.h"
#include "sodium/utils.h"
#include <inttypes.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _login_request_t
{
tf_http_request_t* request;
const char* name;
const char* error;
const char* settings;
const char* code_of_conduct;
bool have_administrator;
bool session_is_new;
char location_header[1024];
const char* set_cookie_header;
int pending;
} login_request_t;
const char* tf_httpd_make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie)
{
const char* k_pattern = "session=%s; path=/; Max-Age=%" PRId64 "; %sSameSite=Strict; HttpOnly";
int length = session_cookie ? snprintf(NULL, 0, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "") : 0;
char* cookie = length ? tf_malloc(length + 1) : NULL;
if (cookie)
{
snprintf(cookie, length + 1, k_pattern, session_cookie, k_httpd_auth_refresh_interval, request->is_tls ? "Secure; " : "");
}
return cookie;
}
static void _login_release(login_request_t* login)
{
int ref_count = --login->pending;
if (ref_count == 0)
{
tf_free((void*)login->name);
tf_free((void*)login->code_of_conduct);
tf_free((void*)login->set_cookie_header);
tf_free(login);
}
}
static void _httpd_endpoint_login_file_read_callback(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (result >= 0)
{
const char* headers[] = {
"Content-Type",
"text/html; charset=utf-8",
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
const char* replace_me = "$AUTH_DATA";
const char* auth = strstr(data, replace_me);
if (auth)
{
JSContext* context = tf_task_get_context(task);
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "session_is_new", JS_NewBool(context, login->session_is_new));
JS_SetPropertyStr(context, object, "name", login->name ? JS_NewString(context, login->name) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "error", login->error ? JS_NewString(context, login->error) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "code_of_conduct", login->code_of_conduct ? JS_NewString(context, login->code_of_conduct) : JS_UNDEFINED);
JS_SetPropertyStr(context, object, "have_administrator", JS_NewBool(context, login->have_administrator));
JSValue object_json = JS_JSONStringify(context, object, JS_NULL, JS_NULL);
size_t json_length = 0;
const char* json = JS_ToCStringLen(context, &json_length, object_json);
char* copy = tf_malloc(result + json_length);
int replace_start = (auth - (const char*)data);
int replace_end = (auth - (const char*)data) + (int)strlen(replace_me);
memcpy(copy, data, replace_start);
memcpy(copy + replace_start, json, json_length);
memcpy(copy + replace_start + json_length, ((const char*)data) + replace_end, result - replace_end);
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, copy, replace_start + json_length + (result - replace_end));
tf_free(copy);
JS_FreeCString(context, json);
JS_FreeValue(context, object_json);
JS_FreeValue(context, object);
}
else
{
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
_login_release(login);
}
static bool _session_is_authenticated_as_user(JSContext* context, JSValue session)
{
bool result = false;
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);
JS_FreeValue(context, user);
return result;
}
static bool _make_administrator_if_first(tf_ssb_t* ssb, JSContext* context, const char* account_name_copy, bool may_become_first_admin)
{
const char* settings = tf_ssb_db_get_property(ssb, "core", "settings");
JSValue settings_value = settings && *settings ? JS_ParseJSON(context, settings, strlen(settings), NULL) : JS_UNDEFINED;
if (JS_IsUndefined(settings_value))
{
settings_value = JS_NewObject(context);
}
bool have_administrator = false;
JSValue permissions = JS_GetPropertyStr(context, settings_value, "permissions");
JSPropertyEnum* ptab = NULL;
uint32_t plen = 0;
JS_GetOwnPropertyNames(context, &ptab, &plen, permissions, JS_GPN_STRING_MASK);
for (int i = 0; i < (int)plen; i++)
{
JSPropertyDescriptor desc = { 0 };
if (JS_GetOwnProperty(context, &desc, permissions, ptab[i].atom) == 1)
{
int permission_length = tf_util_get_length(context, desc.value);
for (int i = 0; i < permission_length; i++)
{
JSValue entry = JS_GetPropertyUint32(context, desc.value, i);
const char* permission = JS_ToCString(context, entry);
if (permission && strcmp(permission, "administration") == 0)
{
have_administrator = true;
}
JS_FreeCString(context, permission);
JS_FreeValue(context, entry);
}
JS_FreeValue(context, desc.setter);
JS_FreeValue(context, desc.getter);
JS_FreeValue(context, desc.value);
}
}
for (uint32_t i = 0; i < plen; ++i)
{
JS_FreeAtom(context, ptab[i].atom);
}
js_free(context, ptab);
if (!have_administrator && may_become_first_admin)
{
if (JS_IsUndefined(permissions))
{
permissions = JS_NewObject(context);
JS_SetPropertyStr(context, settings_value, "permissions", JS_DupValue(context, permissions));
}
JSValue user = JS_GetPropertyStr(context, permissions, account_name_copy);
if (JS_IsUndefined(user))
{
user = JS_NewArray(context);
JS_SetPropertyStr(context, permissions, account_name_copy, JS_DupValue(context, user));
}
JS_SetPropertyUint32(context, user, tf_util_get_length(context, user), JS_NewString(context, "administration"));
JS_FreeValue(context, user);
JSValue settings_json = JS_JSONStringify(context, settings_value, JS_NULL, JS_NULL);
const char* settings_string = JS_ToCString(context, settings_json);
tf_ssb_db_set_property(ssb, "core", "settings", settings_string);
JS_FreeCString(context, settings_string);
JS_FreeValue(context, settings_json);
}
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
tf_free((void*)settings);
return have_administrator;
}
static bool _verify_password(const char* password, const char* hash)
{
char buffer[7 + 22 + 31 + 1];
const char* out_hash = crypt_rn(password, hash, buffer, sizeof(buffer));
return out_hash && strcmp(hash, out_hash) == 0;
}
static void _httpd_endpoint_login_work(tf_ssb_t* ssb, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
const char** form_data = tf_httpd_form_data_decode(request->query, request->query ? strlen(request->query) : 0);
const char* account_name_copy = NULL;
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
if (_session_is_authenticated_as_user(context, jwt))
{
const char* return_url = tf_httpd_form_data_get(form_data, "return");
if (return_url)
{
tf_string_set(login->location_header, sizeof(login->location_header), return_url);
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
goto done;
}
const char* send_session = tf_strdup(session);
bool session_is_new = false;
const char* login_error = NULL;
bool may_become_first_admin = false;
if (strcmp(request->method, "POST") == 0)
{
session_is_new = true;
const char** post_form_data = tf_httpd_form_data_decode(request->body, request->content_length);
const char* submit = tf_httpd_form_data_get(post_form_data, "submit");
if (submit && strcmp(submit, "Login") == 0)
{
const char* account_name = tf_httpd_form_data_get(post_form_data, "name");
account_name_copy = tf_strdup(account_name);
const char* password = tf_httpd_form_data_get(post_form_data, "password");
const char* new_password = tf_httpd_form_data_get(post_form_data, "new_password");
const char* confirm = tf_httpd_form_data_get(post_form_data, "confirm");
const char* change = tf_httpd_form_data_get(post_form_data, "change");
const char* form_register = tf_httpd_form_data_get(post_form_data, "register");
char account_passwd[256] = { 0 };
bool have_account = tf_ssb_db_get_account_password_hash(ssb, tf_httpd_form_data_get(post_form_data, "name"), account_passwd, sizeof(account_passwd));
if (form_register && strcmp(form_register, "1") == 0)
{
bool registered = false;
if (!tf_httpd_is_name_valid(account_name))
{
login_error = "Invalid username. Usernames must contain only letters from the English alphabet and digits and must start with a letter.";
}
else
{
if (!have_account && tf_httpd_is_name_valid(account_name) && password && confirm && strcmp(password, confirm) == 0)
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, account_name, password);
tf_ssb_release_db_writer(ssb, db);
if (registered)
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
}
}
if (!registered && !login_error)
{
login_error = "Error registering account.";
}
}
else if (change && strcmp(change, "1") == 0)
{
bool set = false;
if (have_account && tf_httpd_is_name_valid(account_name) && new_password && confirm && strcmp(new_password, confirm) == 0 &&
_verify_password(password, account_passwd))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
set = tf_ssb_db_set_account_password(tf_ssb_get_loop(ssb), db, context, account_name, new_password);
tf_ssb_release_db_writer(ssb, db);
if (set)
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
}
}
if (!set)
{
login_error = "Error changing password.";
}
}
else
{
if (have_account && *account_passwd && _verify_password(password, account_passwd))
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, account_name);
may_become_first_admin = true;
}
else
{
login_error = "Invalid username or password.";
}
}
}
else
{
tf_free((void*)send_session);
send_session = tf_httpd_make_session_jwt(context, ssb, "guest");
}
tf_free(post_form_data);
}
bool have_administrator = _make_administrator_if_first(ssb, context, account_name_copy, may_become_first_admin);
if (session_is_new && tf_httpd_form_data_get(form_data, "return") && !login_error)
{
const char* return_url = tf_httpd_form_data_get(form_data, "return");
if (return_url)
{
tf_string_set(login->location_header, sizeof(login->location_header), return_url);
}
else
{
snprintf(login->location_header, sizeof(login->location_header), "%s%s/", request->is_tls ? "https://" : "http://", tf_http_request_get_header(request, "host"));
}
login->set_cookie_header = tf_httpd_make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
}
else
{
login->name = account_name_copy;
login->error = login_error;
login->set_cookie_header = tf_httpd_make_set_session_cookie_header(request, send_session);
tf_free((void*)send_session);
login->session_is_new = session_is_new;
login->have_administrator = have_administrator;
login->settings = tf_ssb_db_get_property(ssb, "core", "settings");
if (login->settings)
{
JSValue settings_value = JS_ParseJSON(context, login->settings, strlen(login->settings), NULL);
JSValue code_of_conduct_value = JS_GetPropertyStr(context, settings_value, "code_of_conduct");
const char* code_of_conduct = JS_ToCString(context, code_of_conduct_value);
const char* result = tf_strdup(code_of_conduct);
JS_FreeCString(context, code_of_conduct);
JS_FreeValue(context, code_of_conduct_value);
JS_FreeValue(context, settings_value);
tf_free((void*)login->settings);
login->settings = NULL;
login->code_of_conduct = result;
}
login->pending++;
tf_http_request_ref(request);
tf_file_read(login->request->user_data, "core/auth.html", _httpd_endpoint_login_file_read_callback, login);
account_name_copy = NULL;
}
done:
tf_free((void*)session);
tf_free(form_data);
tf_free((void*)account_name_copy);
JS_FreeValue(context, jwt);
JS_FreeContext(context);
JS_FreeRuntime(runtime);
}
static void _httpd_endpoint_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
login_request_t* login = user_data;
tf_http_request_t* request = login->request;
if (login->pending == 1)
{
if (*login->location_header)
{
const char* headers[] = {
"Location",
login->location_header,
"Set-Cookie",
login->set_cookie_header ? login->set_cookie_header : "",
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
}
tf_http_request_unref(request);
_login_release(login);
}
void tf_httpd_endpoint_login(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
login_request_t* login = tf_malloc(sizeof(login_request_t));
*login = (login_request_t) {
.request = request,
};
login->pending++;
tf_ssb_run_work(ssb, _httpd_endpoint_login_work, _httpd_endpoint_login_after_work, login);
}
void tf_httpd_endpoint_logout(tf_http_request_t* request)
{
const char* k_set_cookie = request->is_tls ? "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly"
: "session=; path=/; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly";
const char* k_location_format = "/login%s%s";
int length = snprintf(NULL, 0, k_location_format, request->query ? "?" : "", request->query);
char* location = alloca(length + 1);
snprintf(location, length + 1, k_location_format, request->query ? "?" : "", request->query ? request->query : "");
const char* headers[] = {
"Set-Cookie",
k_set_cookie,
"Location",
location,
};
tf_http_respond(request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
typedef struct _auto_login_t
{
tf_http_request_t* request;
bool autologin;
const char* users;
} auto_login_t;
static void _httpd_auto_login_work(tf_ssb_t* ssb, void* user_data)
{
auto_login_t* request = user_data;
sqlite3* db = tf_ssb_acquire_db_reader(ssb);
tf_ssb_db_get_global_setting_bool(db, "autologin", &request->autologin);
tf_ssb_release_db_reader(ssb, db);
if (request->autologin)
{
request->users = tf_ssb_db_get_property(ssb, "auth", "users");
if (request->users && strcmp(request->users, "[]") == 0)
{
tf_free((void*)request->users);
request->users = NULL;
}
if (!request->users)
{
JSMallocFunctions funcs = { 0 };
tf_get_js_malloc_functions(&funcs);
JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL);
JSContext* context = JS_NewContext(runtime);
static const char* k_account_name = "mobile";
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
bool registered = tf_ssb_db_register_account(tf_ssb_get_loop(ssb), db, context, k_account_name, k_account_name);
tf_ssb_release_db_writer(ssb, db);
if (registered)
{
_make_administrator_if_first(ssb, context, k_account_name, true);
}
JS_FreeContext(context);
JS_FreeRuntime(runtime);
request->users = tf_ssb_db_get_property(ssb, "auth", "users");
}
}
}
static void _httpd_auto_login_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
auto_login_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
const char* session_token = NULL;
if (!work->autologin)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(work->request, 404, NULL, 0, k_payload, strlen(k_payload));
}
else
{
if (work->users)
{
JSValue json = JS_ParseJSON(context, work->users, strlen(work->users), NULL);
JSValue user = JS_GetPropertyUint32(context, json, 0);
const char* user_string = JS_ToCString(context, user);
session_token = tf_httpd_make_session_jwt(context, ssb, user_string);
JS_FreeCString(context, user_string);
JS_FreeValue(context, user);
JS_FreeValue(context, json);
}
if (session_token)
{
const char* cookie = tf_httpd_make_set_session_cookie_header(work->request, session_token);
tf_free((void*)session_token);
const char* headers[] = {
"Set-Cookie",
cookie,
"Location",
"/",
};
tf_http_respond(work->request, 303, headers, tf_countof(headers) / 2, NULL, 0);
tf_free((void*)cookie);
}
else
{
const char* headers[] = {
"Location",
"/",
};
tf_http_respond(work->request, 303, headers, tf_countof(headers) / 2, NULL, 0);
}
}
tf_http_request_unref(work->request);
tf_free((void*)work->users);
tf_free(work);
}
void tf_httpd_endpoint_login_auto(tf_http_request_t* request)
{
tf_task_t* task = request->user_data;
tf_http_request_ref(request);
tf_ssb_t* ssb = tf_task_get_ssb(task);
auto_login_t* work = tf_malloc(sizeof(auto_login_t));
*work = (auto_login_t) { .request = request };
tf_ssb_run_work(ssb, _httpd_auto_login_work, _httpd_auto_login_after_work, work);
}

181
src/httpd.save.c Normal file
View File

@@ -0,0 +1,181 @@
#include "httpd.js.h"
#include "http.h"
#include "log.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
typedef struct _save_t
{
tf_http_request_t* request;
int response;
char blob_id[k_blob_id_len];
} 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 = tf_httpd_authenticate_jwt(ssb, context, session);
JSValue user = JS_GetPropertyStr(context, jwt, "name");
const char* user_string = JS_ToCString(context, user);
if (user_string && tf_httpd_is_name_valid(user_string))
{
tf_httpd_user_app_t* user_app = tf_httpd_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, NULL, 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[k_blob_id_len] = { 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);
tf_string_set(save->blob_id, sizeof(save->blob_id), blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store or property set failed.\n");
save->response = 500;
}
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 = 403;
}
tf_free(user_app);
}
else if (strcmp(request->path, "/save") == 0)
{
char blob_id[k_blob_id_len] = { 0 };
if (tf_ssb_db_blob_store(ssb, request->body, request->content_length, blob_id, sizeof(blob_id), NULL))
{
tf_string_set(save->blob_id, sizeof(save->blob_id), blob_id);
save->response = 200;
}
else
{
tf_printf("Blob store failed.\n");
save->response = 500;
}
}
else
{
save->response = 400;
}
}
else
{
save->response = 401;
}
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)
{
char body[256] = "";
int length = snprintf(body, sizeof(body), "/%s", save->blob_id);
tf_http_respond(request, 200, NULL, 0, body, length);
}
tf_http_request_unref(request);
tf_free(save);
}
void tf_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);
}

202
src/httpd.static.c Normal file
View File

@@ -0,0 +1,202 @@
#include "httpd.js.h"
#include "file.js.h"
#include "http.h"
#include "mem.h"
#include "task.h"
#include "util.js.h"
#include <assert.h>
#include <stdlib.h>
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
#include <alloca.h>
#endif
typedef struct _http_file_t
{
tf_http_request_t* request;
char etag[512];
} http_file_t;
static bool _ends_with(const char* a, const char* suffix)
{
if (!a || !suffix)
{
return false;
}
size_t alen = strlen(a);
size_t suffixlen = strlen(suffix);
return alen >= suffixlen && strcmp(a + alen - suffixlen, suffix) == 0;
}
static const char* _after(const char* text, const char* prefix)
{
size_t prefix_length = strlen(prefix);
if (text && strncmp(text, prefix, prefix_length) == 0)
{
return text + prefix_length;
}
return NULL;
}
static double _time_spec_to_double(const uv_timespec_t* time_spec)
{
return (double)time_spec->tv_sec + (double)(time_spec->tv_nsec) / 1e9;
}
static void _httpd_endpoint_static_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data)
{
http_file_t* file = user_data;
tf_http_request_t* request = file->request;
if (result >= 0)
{
if (strcmp(path, "core/tfrpc.js") == 0 || _ends_with(path, "core/tfrpc.js"))
{
const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
"etag",
file->etag,
"Access-Control-Allow-Origin",
"null",
};
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
else
{
const char* content_type = tf_httpd_ext_to_content_type(strrchr(path, '.'), true);
const char* headers[] = {
"Content-Type",
content_type,
"etag",
file->etag,
};
tf_http_respond(request, 200, headers, tf_countof(headers) / 2, data, result);
}
}
else
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
}
tf_http_request_unref(request);
tf_free(file);
}
static void _httpd_endpoint_static_stat(tf_task_t* task, const char* path, int result, const uv_stat_t* stat, void* user_data)
{
tf_http_request_t* request = user_data;
const char* match = tf_http_request_get_header(request, "if-none-match");
if (result != 0)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
tf_http_request_unref(request);
}
else
{
char etag[512];
snprintf(etag, sizeof(etag), "\"%f_%zd\"", _time_spec_to_double(&stat->st_mtim), (size_t)stat->st_size);
if (match && strcmp(match, etag) == 0)
{
tf_http_respond(request, 304, NULL, 0, NULL, 0);
tf_http_request_unref(request);
}
else
{
http_file_t* file = tf_malloc(sizeof(http_file_t));
*file = (http_file_t) { .request = request };
static_assert(sizeof(file->etag) == sizeof(etag), "Size mismatch");
memcpy(file->etag, etag, sizeof(etag));
tf_file_read(task, path, _httpd_endpoint_static_read, file);
}
}
}
void tf_httpd_endpoint_static(tf_http_request_t* request)
{
if (request->path && strncmp(request->path, "/.well-known/", strlen("/.well-known/")) && tf_httpd_redirect(request))
{
return;
}
const char* k_static_files[] = {
"index.html",
"client.js",
"tildefriends.svg",
"jszip.min.js",
"style.css",
"tfrpc.js",
"w3.css",
};
const char* k_map[][2] = {
{ "/static/", "core/" },
{ "/lit/", "deps/lit/" },
{ "/codemirror/", "deps/codemirror/" },
{ "/prettier/", "deps/prettier/" },
{ "/speedscope/", "deps/speedscope/" },
{ "/.well-known/", "data/global/.well-known/" },
};
bool is_core = false;
const char* after = NULL;
const char* file_path = NULL;
for (int i = 0; i < tf_countof(k_map) && !after; i++)
{
const char* next_after = _after(request->path, k_map[i][0]);
if (next_after)
{
after = next_after;
file_path = k_map[i][1];
is_core = after && i == 0;
}
}
if ((!after || !*after) && request->path[strlen(request->path) - 1] == '/')
{
after = "index.html";
if (!file_path)
{
file_path = "core/";
is_core = true;
}
}
if (!after || strstr(after, ".."))
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
return;
}
if (is_core)
{
bool found = false;
for (int i = 0; i < tf_countof(k_static_files); i++)
{
if (strcmp(after, k_static_files[i]) == 0)
{
found = true;
break;
}
}
if (!found)
{
const char* k_payload = tf_http_status_text(404);
tf_http_respond(request, 404, NULL, 0, k_payload, strlen(k_payload));
return;
}
}
tf_task_t* task = request->user_data;
const char* root_path = tf_task_get_root_path(task);
size_t size = (root_path ? strlen(root_path) + 1 : 0) + strlen(file_path) + strlen(after) + 1;
char* path = alloca(size);
snprintf(path, size, "%s%s%s%s", root_path ? root_path : "", root_path ? "/" : "", file_path, after);
tf_http_request_ref(request);
tf_file_stat(task, path, _httpd_endpoint_static_stat, request);
}

140
src/httpd.view.c Normal file
View File

@@ -0,0 +1,140 @@
#include "httpd.js.h"
#include "http.h"
#include "mem.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "util.js.h"
typedef struct _view_t
{
tf_http_request_t* request;
const char** form_data;
void* data;
size_t size;
char etag[256];
char notify_want_blob_id[k_blob_id_len];
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[k_blob_id_len] = "";
tf_httpd_user_app_t* user_app = tf_httpd_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);
tf_string_set(blob_id, sizeof(blob_id), 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)
{
snprintf(view->etag, sizeof(view->etag), "\"%s\"", 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) == 0)
{
view->not_modified = true;
}
else
{
if (!tf_ssb_db_blob_get(ssb, blob_id, (uint8_t**)&view->data, &view->size))
{
sqlite3* db = tf_ssb_acquire_db_writer(ssb);
tf_ssb_db_add_blob_wants(db, blob_id);
tf_ssb_release_db_writer(ssb, db);
tf_string_set(view->notify_want_blob_id, sizeof(view->notify_want_blob_id), blob_id);
}
}
}
}
static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
view_t* view = user_data;
const char* filename = tf_httpd_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 ? tf_httpd_magic_bytes_to_content_type(view->data, view->size) : "text/plain",
"etag",
view->etag,
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));
}
if (*view->notify_want_blob_id)
{
tf_ssb_notify_blob_want_added(ssb, view->notify_want_blob_id);
}
tf_free(view->form_data);
tf_http_request_unref(view->request);
tf_free(view);
}
void tf_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 = tf_httpd_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);
}

View File

@@ -13,13 +13,13 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.0.32.1</string> <string>0.2025.9</string>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
<array> <array>
<string>iPhoneOS</string> <string>iPhoneOS</string>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>15</string> <string>17</string>
<key>DTPlatformName</key> <key>DTPlatformName</key>
<string>iphoneos</string> <string>iphoneos</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@@ -1442,7 +1442,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
int result = -1; int result = -1;
tf_task_t* task = tf_task_create(); tf_task_t* task = tf_task_create();
tf_task_set_trusted(task, true); tf_task_set_trusted(task, true);
tf_printf("setting zip path to %s\n", args->zip); tf_printf("Zip path: %s\n", args->zip);
tf_task_set_zip_path(task, args->zip); tf_task_set_zip_path(task, args->zip);
tf_task_set_ssb_network_key(task, args->network_key); tf_task_set_ssb_network_key(task, args->network_key);
tf_task_set_args(task, args->args); tf_task_set_args(task, args->args);
@@ -1487,7 +1487,7 @@ static int _tf_run_task(const tf_run_args_t* args, int index)
break; break;
} }
} }
tf_printf("Using %s as the working directory.\n", cwd); tf_printf("Root path: %s\n", cwd);
} }
if (!*cwd) if (!*cwd)
{ {

View File

@@ -133,6 +133,15 @@ typedef struct _tf_ssb_message_added_callback_node_t
tf_ssb_message_added_callback_node_t* next; tf_ssb_message_added_callback_node_t* next;
} tf_ssb_message_added_callback_node_t; } tf_ssb_message_added_callback_node_t;
typedef struct _tf_ssb_blob_stored_callback_node_t tf_ssb_blob_stored_callback_node_t;
typedef struct _tf_ssb_blob_stored_callback_node_t
{
tf_ssb_blob_stored_callback_t* callback;
tf_ssb_callback_cleanup_t* cleanup;
void* user_data;
tf_ssb_blob_stored_callback_node_t* next;
} tf_ssb_blob_stored_callback_node_t;
typedef struct _tf_ssb_blob_want_added_callback_node_t tf_ssb_blob_want_added_callback_node_t; typedef struct _tf_ssb_blob_want_added_callback_node_t tf_ssb_blob_want_added_callback_node_t;
typedef struct _tf_ssb_blob_want_added_callback_node_t typedef struct _tf_ssb_blob_want_added_callback_node_t
{ {
@@ -235,6 +244,9 @@ typedef struct _tf_ssb_t
tf_ssb_message_added_callback_node_t* message_added; tf_ssb_message_added_callback_node_t* message_added;
int message_added_count; int message_added_count;
tf_ssb_blob_stored_callback_node_t* blob_stored;
int blob_stored_count;
tf_ssb_blob_want_added_callback_node_t* blob_want_added; tf_ssb_blob_want_added_callback_node_t* blob_want_added;
int blob_want_added_count; int blob_want_added_count;
@@ -977,7 +989,6 @@ bool tf_ssb_connection_rpc_send_error_method_not_allowed(tf_ssb_connection_t* co
{ {
char buffer[1024] = ""; char buffer[1024] = "";
snprintf(buffer, sizeof(buffer), "method '%s' is not in list of allowed methods", name); snprintf(buffer, sizeof(buffer), "method '%s' is not in list of allowed methods", name);
tf_printf("%s\n", buffer);
return tf_ssb_connection_rpc_send_error(connection, flags, request_number, buffer); return tf_ssb_connection_rpc_send_error(connection, flags, request_number, buffer);
} }
@@ -2742,6 +2753,17 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
} }
tf_free(node); tf_free(node);
} }
while (ssb->blob_stored)
{
tf_ssb_blob_stored_callback_node_t* node = ssb->blob_stored;
ssb->blob_stored = node->next;
ssb->blob_stored_count--;
if (node->cleanup)
{
node->cleanup(ssb, node->user_data);
}
tf_free(node);
}
while (ssb->blob_want_added) while (ssb->blob_want_added)
{ {
tf_ssb_blob_want_added_callback_node_t* node = ssb->blob_want_added; tf_ssb_blob_want_added_callback_node_t* node = ssb->blob_want_added;
@@ -2784,6 +2806,16 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
uv_run(ssb->loop, UV_RUN_NOWAIT); uv_run(ssb->loop, UV_RUN_NOWAIT);
if (ssb->own_context)
{
if (!ssb->quiet)
{
tf_printf("closing ssb context\n");
}
JS_FreeContext(ssb->context);
JS_FreeRuntime(ssb->runtime);
ssb->own_context = false;
}
if (ssb->loop == &ssb->own_loop) if (ssb->loop == &ssb->own_loop)
{ {
if (!ssb->quiet) if (!ssb->quiet)
@@ -2800,16 +2832,6 @@ void tf_ssb_destroy(tf_ssb_t* ssb)
{ {
tf_printf("uv loop closed.\n"); tf_printf("uv loop closed.\n");
} }
if (ssb->own_context)
{
if (!ssb->quiet)
{
tf_printf("closing ssb context\n");
}
JS_FreeContext(ssb->context);
JS_FreeRuntime(ssb->runtime);
ssb->own_context = false;
}
while (ssb->broadcasts) while (ssb->broadcasts)
{ {
tf_ssb_broadcast_t* broadcast = ssb->broadcasts; tf_ssb_broadcast_t* broadcast = ssb->broadcasts;
@@ -3961,9 +3983,53 @@ void tf_ssb_remove_message_added_callback(tf_ssb_t* ssb, tf_ssb_message_added_ca
} }
} }
void tf_ssb_add_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void (*cleanup)(tf_ssb_t* ssb, void* user_data), void* user_data)
{
tf_ssb_blob_stored_callback_node_t* node = tf_malloc(sizeof(tf_ssb_blob_stored_callback_node_t));
*node = (tf_ssb_blob_stored_callback_node_t) {
.callback = callback,
.cleanup = cleanup,
.user_data = user_data,
.next = ssb->blob_stored,
};
ssb->blob_stored = node;
ssb->blob_stored_count++;
}
void tf_ssb_remove_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void* user_data)
{
tf_ssb_blob_stored_callback_node_t** it = &ssb->blob_stored;
while (*it)
{
if ((*it)->callback == callback && (*it)->user_data == user_data)
{
tf_ssb_blob_stored_callback_node_t* node = *it;
*it = node->next;
ssb->blob_stored_count--;
if (node->cleanup)
{
node->cleanup(ssb, node->user_data);
}
tf_free(node);
}
else
{
it = &(*it)->next;
}
}
}
void tf_ssb_notify_blob_stored(tf_ssb_t* ssb, const char* id) void tf_ssb_notify_blob_stored(tf_ssb_t* ssb, const char* id)
{ {
tf_ssb_blob_stored_callback_node_t* next = NULL;
ssb->blobs_stored++; ssb->blobs_stored++;
for (tf_ssb_blob_stored_callback_node_t* node = ssb->blob_stored; node; node = next)
{
next = node->next;
tf_trace_begin(ssb->trace, "blob stored callback");
node->callback(ssb, id, node->user_data);
tf_trace_end(ssb->trace);
}
} }
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_keys) void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_keys)

View File

@@ -903,10 +903,6 @@ void tf_ssb_db_add_blob_wants(sqlite3* db, const char* id)
{ {
tf_printf("blob wants cache update failed: %s.\n", sqlite3_errmsg(db)); tf_printf("blob wants cache update failed: %s.\n", sqlite3_errmsg(db));
} }
else
{
tf_printf("want: %s\n", id);
}
} }
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }

View File

@@ -384,28 +384,6 @@ void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending)
ebt->send_clock_pending = pending; ebt->send_clock_pending = pending;
} }
void tf_ssb_ebt_debug_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue debug)
{
uv_mutex_lock(&ebt->mutex);
for (int i = 0; i < ebt->entries_count; i++)
{
ebt_entry_t* entry = &ebt->entries[i];
JSValue clock = JS_NewObject(context);
JSValue out = JS_NewObject(context);
JSValue in = JS_NewObject(context);
JS_SetPropertyStr(context, out, "value", JS_NewInt64(context, entry->out));
JS_SetPropertyStr(context, out, "replicate", JS_NewBool(context, entry->out_replicate));
JS_SetPropertyStr(context, out, "receive", JS_NewBool(context, entry->out_receive));
JS_SetPropertyStr(context, clock, "out", out);
JS_SetPropertyStr(context, in, "value", JS_NewInt64(context, entry->in));
JS_SetPropertyStr(context, in, "replicate", JS_NewBool(context, entry->in_replicate));
JS_SetPropertyStr(context, in, "receive", JS_NewBool(context, entry->in_receive));
JS_SetPropertyStr(context, clock, "in", in);
JS_SetPropertyStr(context, debug, entry->id, clock);
}
uv_mutex_unlock(&ebt->mutex);
}
void tf_ssb_ebt_get_progress(tf_ssb_ebt_t* ebt, int* in_pending, int* in_total, int* out_pending, int* out_total) void tf_ssb_ebt_get_progress(tf_ssb_ebt_t* ebt, int* in_pending, int* in_total, int* out_pending, int* out_total)
{ {
uv_mutex_lock(&ebt->mutex); uv_mutex_lock(&ebt->mutex);

View File

@@ -106,15 +106,6 @@ int tf_ssb_ebt_get_send_clock_pending(tf_ssb_ebt_t* ebt);
*/ */
void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending); void tf_ssb_ebt_set_send_clock_pending(tf_ssb_ebt_t* ebt, int pending);
/**
** Get a JSON representation of the clock state for
** debugging.
** @param ebt The EBT instance.
** @param context The JS context.
** @param debug A JS object populated with the information.
*/
void tf_ssb_ebt_debug_clock(tf_ssb_ebt_t* ebt, JSContext* context, JSValue debug);
/** /**
** Get a representation of sync progress. ** Get a representation of sync progress.
** @param ebt The EBT instance. ** @param ebt The EBT instance.

View File

@@ -680,6 +680,31 @@ void tf_ssb_remove_message_added_callback(tf_ssb_t* ssb, tf_ssb_message_added_ca
*/ */
void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_with_keys); void tf_ssb_notify_message_added(tf_ssb_t* ssb, const char* author, int32_t sequence, const char* id, JSValue message_with_keys);
/**
** A callback called when a blob is added to the database.
** @param ssb The SSB instance.
** @param id The blob identifier.
** @param user_data The user data.
*/
typedef void(tf_ssb_blob_stored_callback_t)(tf_ssb_t* ssb, const char* id, void* user_data);
/**
** Register a callback called when a blob is added to the database.
** @param ssb The SSB instance.
** @param callback The callback function.
** @param cleanup A function to call when the callback is removed.
** @param user_data User data to pass to the callback.
*/
void tf_ssb_add_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, tf_ssb_callback_cleanup_t* cleanup, void* user_data);
/**
** Remove a callback registered for when a blob is added to the database.
** @param ssb The SSB instance.
** @param callback The callback function.
** @param user_data User data registered with the callback.
*/
void tf_ssb_remove_blob_stored_callback(tf_ssb_t* ssb, tf_ssb_blob_stored_callback_t* callback, void* user_data);
/** /**
** Record that a new blob was stored. ** Record that a new blob was stored.
** @param ssb The SSB instance. ** @param ssb The SSB instance.

View File

@@ -1082,7 +1082,7 @@ static void _tf_ssb_sqlAsync_work(tf_ssb_t* ssb, void* user_data)
uv_async_send(&sql_work->async); uv_async_send(&sql_work->async);
sqlite3_stmt* statement = NULL; sqlite3_stmt* statement = NULL;
sql_work->result = sqlite3_prepare_v2(db, sql_work->query, -1, &statement, NULL); sql_work->result = sqlite3_prepare_v2(db, sql_work->query, -1, &statement, NULL);
if (sql_work->result == SQLITE_OK) if (sql_work->result == SQLITE_OK && statement)
{ {
const uint8_t* p = sql_work->binds; const uint8_t* p = sql_work->binds;
int column = 0; int column = 0;
@@ -1162,7 +1162,11 @@ static void _tf_ssb_sqlAsync_work(tf_ssb_t* ssb, void* user_data)
} }
} }
sql_work->result = r; sql_work->result = r;
if (r != SQLITE_OK && r != SQLITE_DONE) if (r == SQLITE_MISUSE)
{
sql_work->error = tf_strdup(sqlite3_errstr(sql_work->result));
}
else if (r != SQLITE_OK && r != SQLITE_DONE)
{ {
if (sqlite3_is_interrupted(db)) if (sqlite3_is_interrupted(db))
{ {
@@ -1176,10 +1180,15 @@ static void _tf_ssb_sqlAsync_work(tf_ssb_t* ssb, void* user_data)
_tf_ssb_sql_append(&sql_work->rows, &sql_work->rows_count, &(uint8_t[]) { 0 }, 1); _tf_ssb_sql_append(&sql_work->rows, &sql_work->rows_count, &(uint8_t[]) { 0 }, 1);
sqlite3_finalize(statement); sqlite3_finalize(statement);
} }
else else if (sql_work->result != SQLITE_OK)
{ {
sql_work->error = tf_strdup(sqlite3_errmsg(db)); sql_work->error = tf_strdup(sqlite3_errmsg(db));
} }
else
{
sql_work->result = SQLITE_ERROR;
sql_work->error = tf_strdup("Statement not prepared");
}
uv_mutex_lock(&sql_work->lock); uv_mutex_lock(&sql_work->lock);
sql_work->db = NULL; sql_work->db = NULL;
uv_mutex_unlock(&sql_work->lock); uv_mutex_unlock(&sql_work->lock);
@@ -1618,6 +1627,20 @@ static void _tf_ssb_on_message_added_callback(tf_ssb_t* ssb, const char* author,
JS_FreeValue(context, string); JS_FreeValue(context, string);
} }
static void _tf_ssb_on_blob_stored_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{
JSContext* context = tf_ssb_get_context(ssb);
JSValue callback = JS_MKPTR(JS_TAG_OBJECT, user_data);
JSValue string = JS_NewString(context, id);
JSValue response = JS_Call(context, callback, JS_UNDEFINED, 1, &string);
if (tf_util_report_error(context, response))
{
tf_ssb_remove_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, user_data);
}
JS_FreeValue(context, response);
JS_FreeValue(context, string);
}
static void _tf_ssb_on_blob_want_added_callback(tf_ssb_t* ssb, const char* id, void* user_data) static void _tf_ssb_on_blob_want_added_callback(tf_ssb_t* ssb, const char* id, void* user_data)
{ {
JSContext* context = tf_ssb_get_context(ssb); JSContext* context = tf_ssb_get_context(ssb);
@@ -1738,6 +1761,11 @@ static JSValue _tf_ssb_add_event_listener(JSContext* context, JSValueConst this_
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_add_message_added_callback(ssb, _tf_ssb_on_message_added_callback, _tf_ssb_cleanup_value, ptr); tf_ssb_add_message_added_callback(ssb, _tf_ssb_on_message_added_callback, _tf_ssb_cleanup_value, ptr);
} }
else if (strcmp(event_name, "blob") == 0)
{
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_add_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, _tf_ssb_cleanup_value, ptr);
}
else if (strcmp(event_name, "blob_want_added") == 0) else if (strcmp(event_name, "blob_want_added") == 0)
{ {
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
@@ -1781,6 +1809,11 @@ static JSValue _tf_ssb_remove_event_listener(JSContext* context, JSValueConst th
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_remove_message_added_callback(ssb, _tf_ssb_on_message_added_callback, ptr); tf_ssb_remove_message_added_callback(ssb, _tf_ssb_on_message_added_callback, ptr);
} }
else if (strcmp(event_name, "blob") == 0)
{
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));
tf_ssb_remove_blob_stored_callback(ssb, _tf_ssb_on_blob_stored_callback, ptr);
}
else if (strcmp(event_name, "blob_want_added") == 0) else if (strcmp(event_name, "blob_want_added") == 0)
{ {
void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback)); void* ptr = JS_VALUE_GET_PTR(JS_DupValue(context, callback));

View File

@@ -1561,7 +1561,10 @@ static void _tf_ssb_rpc_delete_blobs_work(tf_ssb_t* ssb, void* user_data)
tf_free(ids); tf_free(ids);
} }
delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL; delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL;
tf_printf("Deleted %d blobs in %d ms.\n", deleted, (int)delete->duration_ms); if (deleted)
{
tf_printf("Deleted %d blobs in %d ms.\n", deleted, (int)delete->duration_ms);
}
} }
static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_rpc_delete_blobs_after_work(tf_ssb_t* ssb, int status, void* user_data)
@@ -1580,7 +1583,6 @@ static void _tf_ssb_rpc_start_delete_blobs_callback(tf_ssb_t* ssb, void* user_da
static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms) static void _tf_ssb_rpc_start_delete_blobs(tf_ssb_t* ssb, int delay_ms)
{ {
tf_printf("will delete more blobs in %d ms\n", delay_ms);
tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_blobs_callback, NULL); tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_blobs_callback, NULL);
} }
@@ -1675,7 +1677,10 @@ static void _tf_ssb_rpc_delete_feeds_work(tf_ssb_t* ssb, void* user_data)
JS_FreeRuntime(runtime); JS_FreeRuntime(runtime);
delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL; delete->duration_ms = (uv_hrtime() - start_ns) / 1000000LL;
tf_printf("Deleted %d message in %d ms.\n", delete->deleted, (int)delete->duration_ms); if (delete->deleted)
{
tf_printf("Deleted %d message in %d ms.\n", delete->deleted, (int)delete->duration_ms);
}
} }
static void _tf_ssb_rpc_delete_feeds_after_work(tf_ssb_t* ssb, int status, void* user_data) static void _tf_ssb_rpc_delete_feeds_after_work(tf_ssb_t* ssb, int status, void* user_data)
@@ -1694,7 +1699,6 @@ static void _tf_ssb_rpc_start_delete_feeds_callback(tf_ssb_t* ssb, void* user_da
static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms) static void _tf_ssb_rpc_start_delete_feeds(tf_ssb_t* ssb, int delay_ms)
{ {
tf_printf("will delete more feeds in %d ms\n", delay_ms);
tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_feeds_callback, NULL); tf_ssb_schedule_work(ssb, delay_ms, _tf_ssb_rpc_start_delete_feeds_callback, NULL);
} }

View File

@@ -152,6 +152,12 @@ static void _wait_stored(tf_ssb_t* ssb, bool* stored)
} }
} }
static void _blob_stored(tf_ssb_t* ssb, const char* id, void* user_data)
{
tf_printf("blob stored %s\n", id);
*(bool*)user_data = true;
}
void tf_ssb_test_ssb(const tf_test_options_t* options) void tf_ssb_test_ssb(const tf_test_options_t* options)
{ {
tf_printf("Testing SSB.\n"); tf_printf("Testing SSB.\n");
@@ -224,8 +230,13 @@ void tf_ssb_test_ssb(const tf_test_options_t* options)
char blob_id[k_id_base64_len] = { 0 }; char blob_id[k_id_base64_len] = { 0 };
const char* k_blob = "Hello, blob!"; const char* k_blob = "Hello, blob!";
bool blob_stored = false;
tf_ssb_add_blob_stored_callback(ssb0, _blob_stored, NULL, &blob_stored);
b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL); b = tf_ssb_db_blob_store(ssb0, (const uint8_t*)k_blob, strlen(k_blob), blob_id, sizeof(blob_id), NULL);
tf_ssb_notify_blob_stored(ssb0, blob_id);
tf_ssb_remove_blob_stored_callback(ssb0, _blob_stored, &blob_stored);
assert(b); assert(b);
assert(blob_stored);
JSContext* context0 = tf_ssb_get_context(ssb0); JSContext* context0 = tf_ssb_get_context(ssb0);
JSValue obj = JS_NewObject(context0); JSValue obj = JS_NewObject(context0);

View File

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

View File

@@ -93,7 +93,7 @@ try:
select(driver, ['#document', 'frame', '#gs_room_name'], ('send_keys', 'test room')) select(driver, ['#document', 'frame', '#gs_room_name'], ('send_keys', 'test room'))
select(driver, ['#document', 'frame', '//*[@id="gs_room_name"]/following-sibling::button'], ('click',)) select(driver, ['#document', 'frame', '//*[@id="gs_room_name"]/following-sibling::button'], ('click',))
select(driver, ['//button[text()="✅ Allow"]'], ('click',)) select(driver, ['//button[text()="✅ Allow"]'], ('click',))
driver.switch_to.alert.accept() wait.until(expected_conditions.alert_is_present()).accept()
select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#identity'], ('click',))
select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',)) select(driver, ['tf-navigation', 'shadow_root', '#logout'], ('click',))

View File

@@ -1,4 +1,4 @@
VERSION=3.3.0 VERSION=3.3.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 -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 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/blog/