Compare commits
453 Commits
Author | SHA1 | Date | |
---|---|---|---|
f748fcf1f7 | |||
9c89c2f717 | |||
d88752d840 | |||
bb565aeb23 | |||
c1015a8bdd | |||
181b21080c | |||
577efb6b7a | |||
1a45113e0c | |||
c49da3db07 | |||
b406501263 | |||
c30b3bbb64 | |||
82f9859c57 | |||
4080266fa3 | |||
210149d6be | |||
c2eb439574 | |||
23a6a24288 | |||
932989ee9c | |||
1a91b56a1d | |||
8115881c08 | |||
7fe3bddeba | |||
376094452e | |||
cd8b32b3ca | |||
2251406bd1 | |||
d8fb956c14 | |||
b1ff215ad7 | |||
f9b4ab91c0 | |||
72952e0c39 | |||
acc14f7318 | |||
d48b8b0ae1 | |||
7ff09ed005 | |||
672fb8fcf4 | |||
fe6d492347 | |||
b65706ffc4 | |||
cb44d408cd | |||
880ab7fdde | |||
be6f24b3ee | |||
902287292d | |||
c4b4103802 | |||
c664f7808f | |||
6b267e472e | |||
dae66424dc | |||
170d5a9621 | |||
179da40a4b | |||
9b696503de | |||
efdecc6017 | |||
1a35a6a161 | |||
041e63ac70 | |||
046bf7e2a9 | |||
20ebdea9d1 | |||
1e84b74ced | |||
cdbc2d48f7 | |||
1140c5ddc7 | |||
0d23294d42 | |||
2dc7f58c80 | |||
de59a7f338 | |||
205f0df1b4 | |||
5ed9a77d38 | |||
ae545e7b2b | |||
e49b54207a | |||
c1df77bb96 | |||
98a7753a55 | |||
d3d4b1a13c | |||
241dfdb90e | |||
6ba41f03da | |||
9ef9dadbb8 | |||
06529fddfb | |||
f015c8727d | |||
3a5ae4c228 | |||
b12f8f9da8 | |||
1abc611e54 | |||
6a4559c580 | |||
54ebd0e643 | |||
60d1ea9d39 | |||
16dbc7617c | |||
a37ad69c8b | |||
3bbeec8ece | |||
de398786be | |||
b8fa59d3ec | |||
704ed737a9 | |||
04ae7a2540 | |||
954e0227d4 | |||
1ab79adb27 | |||
c7ee998b21 | |||
f53ce584e3 | |||
70866e03c8 | |||
656ab7beb6 | |||
1dec53821e | |||
c0a14a738e | |||
d9c5f74d62 | |||
e5dcff0200 | |||
25cc3d7c3a | |||
1cffc5ec24 | |||
5e72b111d9 | |||
3cdfc7af2b | |||
113a82b382 | |||
5b3ae3f006 | |||
c0ecdaae12 | |||
828f61c4e9 | |||
775f00c69c | |||
eadda41518 | |||
8279ec5e9e | |||
ab1f47ee9a | |||
d216d96144 | |||
88592886ca | |||
7077e69bf7 | |||
f983c3d987 | |||
26691051a5 | |||
fe33903e2e | |||
6ea6ae2322 | |||
bb0a840dc6 | |||
52f5bb408f | |||
ee1e1b11af | |||
56db6a8e4d | |||
3b676d967e | |||
97b7643049 | |||
c3fb80a1c8 | |||
7c29c1e18e | |||
4c0dc6ad04 | |||
6cfe0ca4fb | |||
46d3e8f567 | |||
3729346961 | |||
357d944a8d | |||
69991abbb4 | |||
0518c5dd21 | |||
e4c182a6fa | |||
8edc9aaa63 | |||
4525ee9cca | |||
3464f1d189 | |||
fc9c3982c2 | |||
d70dba021a | |||
41590921c3 | |||
4d629c45eb | |||
39f05b6bf5 | |||
58196c4ac0 | |||
eca3696740 | |||
fbfbd6a6b4 | |||
353f2ccc13 | |||
6628a5c420 | |||
1973030774 | |||
787e439524 | |||
fab2c17b43 | |||
1775fdd6b5 | |||
5cc7641788 | |||
6c2fd6d90f | |||
24530e1158 | |||
0bd1463a6b | |||
6728727e89 | |||
ac960a98bf | |||
f787eb077b | |||
b2ecc24e85 | |||
3c82a87968 | |||
f06753b56e | |||
41afc3bdd6 | |||
f764007fc6 | |||
ae5560f33a | |||
e6532979aa | |||
3078536245 | |||
1efc0fd73b | |||
aee99af953 | |||
a154b1c2f6 | |||
7f350a3d87 | |||
982b5817a2 | |||
52f744e106 | |||
7f9c01a9bb | |||
fb3ad0d95d | |||
fe5a6033ef | |||
ff2a0f0c3f | |||
66ea0dadd0 | |||
474ff9cd74 | |||
718383205b | |||
c9e01f220d | |||
f69e74ce53 | |||
b5c6cac048 | |||
515999e570 | |||
ab58f42f0c | |||
af3e96c7e8 | |||
782b5593d5 | |||
d892c9e734 | |||
d3e9041b15 | |||
3a40722c89 | |||
b42b5d11fa | |||
2d8a956c14 | |||
ed6550a4cd | |||
e1ca715c64 | |||
4e3bf99327 | |||
b5b6ed8ba5 | |||
4293e75082 | |||
927e2b7060 | |||
83bdbbb4dc | |||
1dc6084d2d | |||
ae894eaa9d | |||
a8ced8757c | |||
653e16b059 | |||
9c90b2bc1d | |||
cf61e68713 | |||
7b53c95832 | |||
c8e09d8637 | |||
cb9edaacd4 | |||
2992b7e955 | |||
5622db92a7 | |||
58f459fb3b | |||
3bc428a83e | |||
0556af3e07 | |||
2882af1c05 | |||
b06c657ef0 | |||
04ec425c9c | |||
842633f6d1 | |||
e5160b9d2c | |||
e8fb73fdf9 | |||
939e13c3c8 | |||
787e929747 | |||
b688a89b66 | |||
7848b5e560 | |||
87224d2bb6 | |||
2826efea56 | |||
0d1b231344 | |||
804359d12e | |||
11ad344e52 | |||
d802c0023b | |||
42fcfee042 | |||
a1d244567a | |||
00bdf1df4a | |||
9b2d4b393d | |||
5e0c20e432 | |||
352f33f5a1 | |||
efc5eb2aff | |||
7e9460f47c | |||
41cabad264 | |||
b488db9137 | |||
cb315c717b | |||
3381b588a1 | |||
7c2962afcf | |||
498a093cde | |||
07a87ff9de | |||
c138582638 | |||
011038a38a | |||
1bfa18b8d7 | |||
95f0b91a0e | |||
ffaaec5b37 | |||
ac0482d7f5 | |||
f4b46cc3a0 | |||
4bb095e81f | |||
5e92e2ffe1 | |||
a4a0745385 | |||
eb191254b0 | |||
e4e763b7a0 | |||
5ffc505ce2 | |||
1bdd67d659 | |||
483638a7e6 | |||
50bef73200 | |||
d4135f7133 | |||
557ae6ee5a | |||
07b4f2b08f | |||
9a75af8146 | |||
5b3c7dcecc | |||
6b20d69976 | |||
fbb61581c6 | |||
25d793e9e8 | |||
8f35004a01 | |||
e59eb66c1d | |||
412dce0a47 | |||
1aa4b0e590 | |||
ef9e42e030 | |||
059024452c | |||
39a1acaf38 | |||
8ecc07452e | |||
e85ee5766b | |||
91339dc8a7 | |||
7733cb2604 | |||
157209e9b5 | |||
cd51edcd8f | |||
a98a848bb7 | |||
c57b0a2f2f | |||
bf7d5c34f6 | |||
ea92fbdcea | |||
9f75346dd8 | |||
b5111efc29 | |||
0278aceb62 | |||
ec5d7c1a01 | |||
40216377f9 | |||
d062db2ba8 | |||
d3875cf738 | |||
fefb0f92bc | |||
0ddb86b5a8 | |||
d77c452120 | |||
e84ced6f79 | |||
e4d77679dc | |||
9fd4be0e4a | |||
7b32067b07 | |||
e1167b6854 | |||
25ee0a3561 | |||
4771810d6b | |||
ae10d3fa6f | |||
ac92b5f8de | |||
d0c89991be | |||
0a580b60b1 | |||
24116f498f | |||
5623cba7c3 | |||
bd81b2acf5 | |||
6c28ca738e | |||
b2a552b3e0 | |||
0f03701043 | |||
d470d6c398 | |||
98de9b037a | |||
df0bb102dc | |||
1734c88627 | |||
df94378b96 | |||
83fa488b8d | |||
1515525a1b | |||
0b5017b208 | |||
c864041fa0 | |||
e9e1a3e80d | |||
1ddaa7deb0 | |||
cf56078e25 | |||
a156cdea9f | |||
4637509b3d | |||
1ae9aaf752 | |||
11cd707382 | |||
1807264df5 | |||
e927ff915b | |||
b8068b9ed1 | |||
019ab99ecc | |||
c40a513876 | |||
dbfa9e5623 | |||
5e0304481b | |||
0bcc7d8c59 | |||
27c2f27708 | |||
e1f868730f | |||
7ba1e6980f | |||
35b7eb511a | |||
d51eb64c8e | |||
53aac8d23a | |||
9d105bdc1a | |||
873019f054 | |||
7f8155613c | |||
5f96eb18b2 | |||
32c7fcbbfa | |||
c28d4d9378 | |||
6af9c17efe | |||
ec9e9151dc | |||
86aa5e4d1e | |||
26150f98e1 | |||
bb81fc87b9 | |||
700d09c730 | |||
50d860183d | |||
1cf55d7d64 | |||
ae84f69025 | |||
49ffd1055e | |||
e2c25ab414 | |||
c02a3d3659 | |||
24cf18651a | |||
3eabe72299 | |||
e1448a1c3a | |||
df5dfa1539 | |||
23b15a8dc5 | |||
d550092bd3 | |||
4268963e70 | |||
3026443c1e | |||
4e359c3f5c | |||
4f1b31bce0 | |||
b980bb4946 | |||
fafc524c8c | |||
0cab3e7ed9 | |||
12010a84a3 | |||
f7974d2cef | |||
62e9dfea90 | |||
0bf216bb1a | |||
aba95d4fe8 | |||
5e205ac897 | |||
0f7472fa22 | |||
12ab2f4b85 | |||
f676cd937f | |||
263a59f6c5 | |||
2e1e4f90e7 | |||
6eed168b7d | |||
9f0315458f | |||
c590eb3a44 | |||
2e1b0089ae | |||
05b55c849a | |||
5fbe9c42bc | |||
3cddc524d1 | |||
efcada8e25 | |||
c616a16993 | |||
d4f7fdfc40 | |||
f760d48368 | |||
ba87f9acaa | |||
9faa4c9ca6 | |||
58b0b54785 | |||
e3b83c10db | |||
7f31798119 | |||
d9ffca81f8 | |||
1dd3b3c9aa | |||
8075bdfe99 | |||
b15cf901ad | |||
84a3d7348d | |||
00c1ec660e | |||
9e1bab03eb | |||
63c344112d | |||
18c90214a8 | |||
68cf3efcde | |||
308e24c9fa | |||
2fb7fceb0c | |||
fde7fb4270 | |||
03a2367532 | |||
08cd0ec878 | |||
0a01332d1f | |||
256c47c33c | |||
62ad08985c | |||
21ba7cb02c | |||
77ec1a0b2e | |||
07a0828626 | |||
08e32c0de4 | |||
f4f6bb8333 | |||
b1a6384ac1 | |||
786c83c57c | |||
843c53e15e | |||
470814f147 | |||
24a91219c1 | |||
d3e02470cd | |||
6d2b560c3d | |||
059392df8e | |||
3b4f0c1321 | |||
a09d159268 | |||
91ec68252d | |||
e85168ac53 | |||
35e0d8b68a | |||
cadcb236ee | |||
cfd5341a6b | |||
e922af4c55 | |||
45dfe34375 | |||
c78d3b0413 | |||
dd90fe4fbf | |||
be6a39bd15 | |||
da51e87774 | |||
5197eb91f7 | |||
87747c0b6b | |||
03cf347394 | |||
3487f335e5 | |||
8c0d380a4d | |||
b660abff7f | |||
5988fddf8d | |||
f268ca3adf | |||
cbc21cfbe6 | |||
cf195cdd44 | |||
85c5b4c4d6 | |||
7d8258c262 | |||
92c06b34a9 | |||
7012418b13 | |||
2b5a56abfe | |||
d8657866f5 | |||
a2851f8ade | |||
ff4c144be3 | |||
3650cd8350 |
.dockerignoreCOPYINGDockerfileLICENSEMakefileREADME.md
apps/cory
admin.json
admin
api.jsonapi
apps.jsonapps
db.jsondb
docs.jsondocs
follow.jsonfollow
index.jsonindex
ssb.jsonssb
app.jscommonmark-hashtag.jscommonmark-linkify.jscommonmark.min.jsemojis.jsemojis.jsonindex.htmllit-all.min.jslit-all.min.js.mapscript.jstf-app.jstf-compose.jstf-connections.jstf-id-picker.jstf-message.jstf-news.jstf-profile.jstf-styles.jstf-tab-connections.jstf-tab-news.jstf-tab-search.jstf-user.jstf-utils.jstribute.csstribute.esm.js
todo.jsontodo
core
app.jsauth.htmlauth.jsclient.jscore.jsencoding-indexes.jsencoding.jsform.jshttp.jshttpd.jsindex.htmlsha1.jsssb.jsstyle.csstfrpc.js
deps
codemirror
annotatescrollbar.min.jsbase16-dark.min.csscodemirror.min.csscodemirror.min.jscss.min.jsdialog.min.cssdialog.min.jshtmlmixed.min.jsjavascript.min.jsjump-to-line.min.jsmatchesonscrollbar.min.cssmatchesonscrollbar.min.jssearch.min.jssearchcursor.min.jstrailingspace.min.jsxml.min.js
libbacktrace
.gitignoreIsaac.Newton-Opticks.txtLICENSEMakefile.amMakefile.inREADME.mdaclocal.m4alloc.callocfail.callocfail.shatomic.cbacktrace-supported.h.inbacktrace.cbacktrace.hbtest.ccompileconfig.guessconfig.h.inconfig.sub
config
enable.m4lead-dot.m4libtool.m4ltoptions.m4ltsugar.m4ltversion.m4lt~obsolete.m4multi.m4override.m4unwind_ipinfo.m4warnings.m4
configureconfigure.acdwarf.cedtest.cedtest2.celf.cfileline.cfilenames.hfiletype.awkinstall-debuginfo-for-buildid.sh.ininstall-shinstrumented_alloc.cinternal.hlibtool.m4ltmain.shltoptions.m4ltsugar.m4ltversion.m4lt~obsolete.m4macho.cmissingmmap.cmmapio.cmove-if-changemtest.cnounwind.cpecoff.cposix.cprint.cread.csimple.csort.cstate.cstest.ctest-drivertest_format.ctestlib.ctestlib.httest.cunittest.cunknown.cxcoff.cxztest.cztest.clibbacktrace_config
libsodium
.github
.gitignoreAUTHORSChangeLogLICENSEMakefile.amMakefile.inREADME.markdownTHANKSaclocal.m4appveyor.ymlautogen.shazure-pipelines.ymlbuild-aux
build.zigbuilds
.gitignoreMakefile.amMakefile.in
configureconfigure.acmsvc
build
properties
Common.propsDLL.propsDebug.propsDebugDEXE.propsDebugDLL.propsDebugLEXE.propsDebugLIB.propsDebugLTCG.propsDebugSEXE.propsEXE.propsLIB.propsLTCG.propsLink.propsMessages.propsOutput.propsRelease.propsReleaseDEXE.propsReleaseDLL.propsReleaseLEXE.propsReleaseLIB.propsReleaseLTCG.propsReleaseSEXE.propsWin32.propsx64.props
resource.hresource.rcversion.hvs2010
vs2012
vs2013
vs2015
vs2017
vs2019
vs2022
contrib
dist-build
Makefile.amMakefile.inandroid-arm.shandroid-armv7-a.shandroid-armv8-a.shandroid-build.shandroid-mips32.shandroid-mips64.shandroid-x86.shandroid-x86_64.shapple-xcframework.shemscripten-symbols.defemscripten.shgenerate-emscripten-symbols.shios.shmsys2-win32.shmsys2-win64.shosx.shwasm32-wasi.shwatchos.sh
lgtm.ymllibsodium-uninstalled.pc.inlibsodium.pc.inlibsodium.slnlibsodium.vcxprojlibsodium.vcxproj.filterslogo.pngm4
ax_add_fortify_source.m4ax_check_catchable_abrt.m4ax_check_catchable_segv.m4ax_check_compile_flag.m4ax_check_define.m4ax_check_gnu_make.m4ax_check_link_flag.m4ax_pthread.m4ax_tls.m4ax_valgrind_check.m4ld-output-def.m4libtool.m4ltoptions.m4ltsugar.m4ltversion.m4lt~obsolete.m4
msvc-scripts
packaging
regen-msvc
libsodium.vcxprojlibsodium.vcxproj.filterslibsodium.vcxproj.filters.tpllibsodium.vcxproj.tplregen-msvc.pytl_libsodium.vcxproj.filters.tpltl_libsodium.vcxproj.tpl
src
Makefile.amMakefile.in
libsodium
Makefile.amMakefile.incrypto_stream.c
crypto_aead
aes256gcm
chacha20poly1305
sodium
xchacha20poly1305
sodium
crypto_auth
crypto_box
crypto_core
crypto_generichash
crypto_hash
crypto_kdf
crypto_kx
crypto_onetimeauth
crypto_onetimeauth.c
poly1305
crypto_pwhash
argon2
argon2-core.cargon2-core.hargon2-encoding.cargon2-encoding.hargon2-fill-block-avx2.cargon2-fill-block-avx512f.cargon2-fill-block-ref.cargon2-fill-block-ssse3.cargon2.cargon2.hblake2b-long.cblake2b-long.hblamka-round-avx2.hblamka-round-avx512f.hblamka-round-ref.hblamka-round-ssse3.hpwhash_argon2i.cpwhash_argon2id.c
crypto_pwhash.cscryptsalsa208sha256
crypto_scalarmult
crypto_scalarmult.c
curve25519
ref10
sandy2x
consts.Sconsts_namespace.hcurve25519_sandy2x.ccurve25519_sandy2x.hfe.hfe51.hfe51_invert.cfe51_mul.Sfe51_namespace.hfe51_nsquare.Sfe51_pack.Sfe_frombytes_sandy2x.cladder.Sladder.hladder_namespace.hsandy2x.S
scalarmult_curve25519.cscalarmult_curve25519.hed25519
ristretto255
crypto_secretbox
crypto_secretstream
xchacha20poly1305
crypto_shorthash
crypto_shorthash.c
siphash24
crypto_sign
crypto_stream
chacha20
dolbeau
chacha20_dolbeau-avx2.cchacha20_dolbeau-avx2.hchacha20_dolbeau-ssse3.cchacha20_dolbeau-ssse3.hu0.hu1.hu4.hu8.h
ref
stream_chacha20.cstream_chacha20.hsalsa20
salsa2012
salsa208
xchacha20
xsalsa20
crypto_verify
sodium
include
Makefile.amMakefile.insodium.h
sodium
core.hcrypto_aead_aes256gcm.hcrypto_aead_chacha20poly1305.hcrypto_aead_xchacha20poly1305.hcrypto_auth.hcrypto_auth_hmacsha256.hcrypto_auth_hmacsha512.hcrypto_auth_hmacsha512256.hcrypto_box.hcrypto_box_curve25519xchacha20poly1305.hcrypto_box_curve25519xsalsa20poly1305.hcrypto_core_ed25519.hcrypto_core_hchacha20.hcrypto_core_hsalsa20.hcrypto_core_ristretto255.hcrypto_core_salsa20.hcrypto_core_salsa2012.hcrypto_core_salsa208.hcrypto_generichash.hcrypto_generichash_blake2b.hcrypto_hash.hcrypto_hash_sha256.hcrypto_hash_sha512.hcrypto_kdf.hcrypto_kdf_blake2b.hcrypto_kx.hcrypto_onetimeauth.hcrypto_onetimeauth_poly1305.hcrypto_pwhash.hcrypto_pwhash_argon2i.hcrypto_pwhash_argon2id.hcrypto_pwhash_scryptsalsa208sha256.hcrypto_scalarmult.hcrypto_scalarmult_curve25519.hcrypto_scalarmult_ed25519.hcrypto_scalarmult_ristretto255.hcrypto_secretbox.hcrypto_secretbox_xchacha20poly1305.hcrypto_secretbox_xsalsa20poly1305.hcrypto_secretstream_xchacha20poly1305.hcrypto_shorthash.hcrypto_shorthash_siphash24.hcrypto_sign.hcrypto_sign_ed25519.hcrypto_sign_edwards25519sha512batch.hcrypto_stream.hcrypto_stream_chacha20.hcrypto_stream_salsa20.hcrypto_stream_salsa2012.hcrypto_stream_salsa208.hcrypto_stream_xchacha20.hcrypto_stream_xsalsa20.hcrypto_verify_16.hcrypto_verify_32.hcrypto_verify_64.hexport.h
private
chacha20_ietf_ext.hcommon.hed25519_ref10.hed25519_ref10_fe_25_5.hed25519_ref10_fe_51.himplementations.hmutex.hsse2_64_32.h
randombytes.hrandombytes_internal_random.hrandombytes_sysrandom.hruntime.hutils.hversion.h.inrandombytes
sodium
test
Makefile.amMakefile.inconstcheck.sh
default
Makefile.amMakefile.inaead_aes256gcm.caead_aes256gcm.expaead_aes256gcm2.caead_aes256gcm2.expaead_chacha20poly1305.caead_chacha20poly1305.expaead_chacha20poly13052.caead_chacha20poly13052.expaead_xchacha20poly1305.caead_xchacha20poly1305.expauth.cauth.expauth2.cauth2.expauth3.cauth3.expauth5.cauth5.expauth6.cauth6.expauth7.cauth7.expbox.cbox.expbox2.cbox2.expbox7.cbox7.expbox8.cbox8.expbox_easy.cbox_easy.expbox_easy2.cbox_easy2.expbox_seal.cbox_seal.expbox_seed.cbox_seed.expchacha20.cchacha20.expcmptest.hcodecs.ccodecs.expcore1.ccore1.expcore2.ccore2.expcore3.ccore3.expcore4.ccore4.expcore5.ccore5.expcore6.ccore6.expcore_ed25519.ccore_ed25519.expcore_ristretto255.ccore_ristretto255.exped25519_convert.ced25519_convert.expgenerichash.cgenerichash.expgenerichash2.cgenerichash2.expgenerichash3.cgenerichash3.exphash.chash.exphash3.chash3.expindex.html.tplkdf.ckdf.expkeygen.ckeygen.expkx.ckx.expmetamorphic.cmetamorphic.expmisuse.cmisuse.exponetimeauth.conetimeauth.exponetimeauth2.conetimeauth2.exponetimeauth7.conetimeauth7.exppre.js.incpwhash_argon2i.cpwhash_argon2i.exppwhash_argon2id.cpwhash_argon2id.exppwhash_scrypt.cpwhash_scrypt.exppwhash_scrypt_ll.cpwhash_scrypt_ll.exprandombytes.crandombytes.expscalarmult.cscalarmult.expscalarmult2.cscalarmult2.expscalarmult5.cscalarmult5.expscalarmult6.cscalarmult6.expscalarmult7.cscalarmult7.expscalarmult8.cscalarmult8.expscalarmult_ed25519.cscalarmult_ed25519.expscalarmult_ristretto255.cscalarmult_ristretto255.expsecretbox.csecretbox.expsecretbox2.csecretbox2.expsecretbox7.csecretbox7.expsecretbox8.csecretbox8.expsecretbox_easy.csecretbox_easy.expsecretbox_easy2.csecretbox_easy2.expsecretstream.csecretstream.expshorthash.cshorthash.expsign.csign.expsiphashx24.csiphashx24.expsodium_core.csodium_core.expsodium_utils.csodium_utils.expsodium_utils2.csodium_utils2.expsodium_utils3.csodium_utils3.expsodium_version.csodium_version.expstream.cstream.expstream2.cstream2.expstream3.cstream3.expstream4.cstream4.expverify1.cverify1.expwasi-test-wrapper.shwintest.batxchacha20.cxchacha20.exp
quirks
libuv
.github
.mailmap.readthedocs.yamlAUTHORSCMakeLists.txtCONTRIBUTING.mdChangeLogLICENSELINKS.mdMAINTAINERS.mdMakefile.amREADME.mdSUPPORTED_PLATFORMS.mdautogen.shconfigure.acdocs
code
requirements.txtsrc
include
m4
src
fs-poll.cidna.cinet.cstrscpy.hstrtok.cstrtok.hthreadpool.ctimer.c
unix
android-ifaddrs.casync.catomic-ops.hbsd-ifaddrs.cbsd-proctitle.ccore.cdarwin.cdl.cepoll.cfreebsd.cfs.cfsevents.cgetaddrinfo.churd.cibmi.cinternal.hkqueue.clinux-core.clinux-inotify.clinux-syscalls.clinux-syscalls.hos390-proctitle.cos390-syscalls.cos390-syscalls.hos390.cpipe.cpoll.cprocess.cproctitle.csignal.cstream.csunos.ctcp.cthread.ctty.cudp.c
uv-common.cuv-common.hwin
test
benchmark-async-pummel.cbenchmark-async.cbenchmark-list.hbenchmark-million-async.cbenchmark-million-timers.cbenchmark-multi-accept.cbenchmark-ping-udp.cbenchmark-pound.cbenchmark-pump.cbenchmark-queue-work.cbenchmark-tcp-write-batch.cbenchmark-udp-pummel.cblackhole-server.cdns-server.cecho-server.crun-benchmarks.crun-tests.crunner-unix.crunner.ctask.htest-active.ctest-async.ctest-callback-stack.ctest-close-fd.ctest-close-order.ctest-default-loop-close.ctest-delayed-accept.ctest-dlerror.ctest-embed.ctest-error.ctest-fs-event.ctest-fs-open-flags.ctest-fs-poll.ctest-fs-readdir.ctest-fs.ctest-get-currentexe.ctest-get-passwd.ctest-getaddrinfo.ctest-getnameinfo.ctest-getsockname.ctest-getters-setters.ctest-idle.ctest-idna.ctest-ip-name.ctest-ip6-addr.ctest-ipc-heavy-traffic-deadlock-bug.ctest-ipc-send-recv.ctest-ipc.ctest-list.htest-loop-handles.ctest-loop-time.ctest-multiple-listen.ctest-not-readable-nor-writable-on-read-error.ctest-not-writable-after-shutdown.ctest-ping-pong.ctest-pipe-bind-error.ctest-pipe-connect-error.ctest-pipe-connect-prepare.ctest-pipe-getsockname.ctest-pipe-server-close.ctest-pipe-set-non-blocking.ctest-platform-output.ctest-poll-multiple-handles.ctest-poll.ctest-readable-on-eof.ctest-ref.ctest-shutdown-eof.ctest-shutdown-simultaneous.ctest-signal-multiple-loops.ctest-signal-pending-on-close.ctest-spawn.ctest-strtok.ctest-tcp-alloc-cb-fail.ctest-tcp-bind-error.ctest-tcp-bind6-error.ctest-tcp-close-after-read-timeout.ctest-tcp-close-reset.ctest-tcp-close.ctest-tcp-connect-error.ctest-tcp-connect-timeout.ctest-tcp-connect6-error.ctest-tcp-create-socket-early.ctest-tcp-open.ctest-tcp-rst.ctest-tcp-write-fail.ctest-tcp-write-to-half-open-connection.ctest-tcp-writealot.ctest-thread-equal.ctest-thread.ctest-threadpool-cancel.ctest-timer-again.ctest-timer.ctest-tty-duplicate-key.ctest-tty-escape-sequence-processing.ctest-tty.ctest-udp-alloc-cb-fail.ctest-udp-connect.ctest-udp-connect6.ctest-udp-mmsg.ctest-udp-multicast-interface.ctest-udp-multicast-interface6.ctest-udp-multicast-join.ctest-udp-multicast-join6.ctest-udp-multicast-ttl.ctest-udp-open.ctest-udp-send-and-recv.ctest-udp-send-hang-loop.ctest-udp-send-immediate.ctest-udp-send-unreachable.ctest-udp-try-send.c
tools
quickjs
ChangelogLICENSEMakefileTODOVERSION
doc
libbf.clibbf.hqjs.cqjsc.cqjscalc.jsquickjs-libc.cquickjs.cquickjs.hrelease.shrun-test262.ctest262.conftest262_errors.txttests
smoothie
speedscope
LICENSEREADMEdemangle-cpp.1768f4cc.jsdemangle-cpp.1768f4cc.js.mapfavicon-16x16.f74b3187.pngfavicon-32x32.bc503437.pngfile-format-schema.jsonimport.a03bf119.jsimport.a03bf119.js.mapindex.htmlperf-vertx-stacks-01-collapsed-all.2681da68.txtrelease.txtreset.8c46b7a1.cssreset.8c46b7a1.css.mapsource-map.438fa06b.jssource-map.438fa06b.js.mapspeedscope.eee21de6.jsspeedscope.eee21de6.js.map
split
sqlite
valgrind
src
bcrypt.hbcrypt.js.cbcrypt.js.hdatabase.cdatabase.js.cdatabase.js.hfile.cfile.hfile.js.cfile.js.hmain.cmem.cmem.hpacketstream.cpacketstream.hserialize.csocket.js.csocket.js.hssb.cssb.connections.cssb.db.cssb.db.hssb.export.cssb.export.hssb.hssb.import.cssb.import.hssb.js.cssb.js.hssb.qjs.cssb.rpc.cssb.rpc.hssb.tests.cssb.tests.htask.ctask.htaskstub.js.ctaskstub.js.htests.ctests.htls.ctlscontext.js.ctlscontext.js.htlscontextwrapper.ctlscontextwrapper.htrace.ctrace.hutil.js.cutil.js.h
tests
01-nop02-valgrind03-child04-promise05-promise-remote-throw07-promise-remote-reject08-database09-this10-await11-require12-exit13-icu14-uint8array15-socket
tools
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.svn
|
||||||
|
db.sqlite
|
||||||
|
out/**/*.o
|
||||||
|
out/**/*.d
|
59
COPYING
59
COPYING
@ -1,59 +0,0 @@
|
|||||||
Tilde Friends - An operating system for the web.
|
|
||||||
Copyright (C) 2014 Cory McWilliams <cory@unprompted.com>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as
|
|
||||||
published by the Free Software Foundation, either version 3 of the
|
|
||||||
License, or (at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Additional permission under GNU GPL version 3 section 7
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with libuv (or a modified version of that library), containing parts covered by
|
|
||||||
the terms of the MIT License, the licensors of this Program grant you
|
|
||||||
additional permission to convey the resulting work. {Corresponding Source for
|
|
||||||
a non-source form of such a combination shall include the source code for the
|
|
||||||
parts of libuv used as well as that of the covered work.}
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with QuickJS (or a modified version of that library), containing parts covered
|
|
||||||
by the terms of the MIT License, the licensors of this Program grant you
|
|
||||||
additional permission to convey the resulting work. {Corresponding Source for
|
|
||||||
a non-source form of such a combination shall include the source code for the
|
|
||||||
parts of QuickJS used as well as that of the covered work.}
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with libsodium (or a modified version of that library), containing parts
|
|
||||||
covered by the terms of the ISC License, the licensors of this Program grant
|
|
||||||
you additional permission to convey the resulting work. {Corresponding Source
|
|
||||||
for a non-source form of such a combination shall include the source code for
|
|
||||||
the parts of libsodium used as well as that of the covered work.}
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with xopt (or a modified version of that library), containing parts covered by
|
|
||||||
the terms of the Apache License 2.0, the licensors of this Program grant you
|
|
||||||
additional permission to convey the resulting work. {Corresponding Source for
|
|
||||||
a non-source form of such a combination shall include the source code for the
|
|
||||||
parts of xopt used as well as that of the covered work.}
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with crypt_blowfish (or a modified version of that library), containing parts
|
|
||||||
covered by the terms of the MIT License, the licensors of this Program grant
|
|
||||||
you additional permission to convey the resulting work. {Corresponding Source
|
|
||||||
for a non-source form of such a combination shall include the source code for
|
|
||||||
the parts of crypt_blowfish used as well as that of the covered work.}
|
|
||||||
|
|
||||||
If you modify this Program, or any covered work, by linking or combining it
|
|
||||||
with base64c (or a modified version of that library), containing parts covered
|
|
||||||
by the terms of the BSD 3-Clause License, the licensors of this Program grant
|
|
||||||
you additional permission to convey the resulting work. {Corresponding Source
|
|
||||||
for a non-source form of such a combination shall include the source code for
|
|
||||||
the parts of base64c used as well as that of the covered work.}
|
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM bitnami/minideb:bullseye AS build
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libssl-dev
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
RUN make -C /app -j $(nproc) release
|
||||||
|
|
||||||
|
FROM bitnami/minideb:bullseye
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libssl1.1
|
||||||
|
|
||||||
|
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
|
||||||
|
COPY --from=build /app/apps /app/apps
|
||||||
|
COPY --from=build /app/core /app/core
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 12345
|
||||||
|
ENTRYPOINT ["/app/out/release/tildefriends"]
|
680
LICENSE
680
LICENSE
@ -1,661 +1,19 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
Copyright 2014 Cory McWilliams
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
the Software without restriction, including without limitation the rights to
|
||||||
of this license document, but changing it is not allowed.
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
Preamble
|
so, subject to the following conditions:
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The above copyright notice and this permission notice shall be included in all
|
||||||
software and other kinds of works, specifically designed to ensure
|
copies or substantial portions of the Software.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
The licenses for most software and other practical works are designed
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
to take away your freedom to share and change the works. By contrast,
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
share and change all versions of a program--to make sure it remains free
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
software for all its users.
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
|
274
Makefile
274
Makefile
@ -1,63 +1,88 @@
|
|||||||
|
.ONESHELL:
|
||||||
|
.DELETE_ON_ERROR:
|
||||||
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
PROJECT = tildefriends
|
PROJECT = tildefriends
|
||||||
BUILD_DIR ?= out
|
BUILD_DIR ?= out
|
||||||
|
BUILD_TYPES := debug release windebug winrelease
|
||||||
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
COMMON_CFLAGS = \
|
CFLAGS += \
|
||||||
-Wall \
|
-Wall \
|
||||||
-Werror \
|
|
||||||
-Wextra \
|
-Wextra \
|
||||||
-Wno-unused-parameter \
|
-Wno-unused-parameter \
|
||||||
-Wno-cast-function-type \
|
-Wno-cast-function-type \
|
||||||
-MMD \
|
-MMD \
|
||||||
-ffunction-sections \
|
-ffunction-sections \
|
||||||
-fdata-sections
|
-fdata-sections \
|
||||||
COMMON_LDFLAGS += -Wl,-gc-sections
|
-fno-omit-frame-pointer \
|
||||||
|
-g
|
||||||
|
LDFLAGS += -Wl,-gc-sections
|
||||||
|
|
||||||
ifneq ($(UNUSED),)
|
debug windebug: CFLAGS += -Og
|
||||||
COMMON_LDFLAGS += -Wl,-print-gc-sections
|
debug release: LDFLAGS += -rdynamic
|
||||||
|
release winrelease: CFLAGS += -DNDEBUG -O3
|
||||||
|
windebug winrelease: CC = i686-w64-mingw32-gcc-win32
|
||||||
|
windebug winrelease: AS = $(CC)
|
||||||
|
windebug winrelease: CFLAGS += -D_WIN32_WINNT=0x0A00 -DWINVER=0x0A00 -DNTDDI_VERSION=NTDDI_WIN10
|
||||||
|
windebug winrelease: LDFLAGS += -static
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifneq ($(DEBUG),)
|
get_objs = \
|
||||||
COMMON_CFLAGS += -g -fsanitize=address -fsanitize=undefined
|
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
||||||
COMMON_LDFLAGS += -fsanitize=address -fsanitize=undefined
|
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||||
BUILD_DIR := $(BUILD_DIR)/debug
|
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win)))))
|
||||||
else
|
|
||||||
COMMON_CFLAGS += -DNDEBUG -O3
|
|
||||||
BUILD_DIR := $(BUILD_DIR)/release
|
|
||||||
endif
|
|
||||||
|
|
||||||
APP_BIN = $(BUILD_DIR)/$(PROJECT)
|
APP_SOURCES := $(wildcard src/*.c)
|
||||||
APP_SOURCES = $(wildcard src/*.c)
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
APP_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(APP_SOURCES))
|
|
||||||
$(APP_OBJS): CFLAGS += \
|
$(APP_OBJS): CFLAGS += \
|
||||||
-Ideps/base64c/include \
|
-Ideps/base64c/include \
|
||||||
-Ideps/crypt_blowfish \
|
-Ideps/crypt_blowfish \
|
||||||
|
-Ideps/libbacktrace \
|
||||||
|
-Ideps/libsodium \
|
||||||
|
-Ideps/libsodium/src/libsodium/include \
|
||||||
|
-Ideps/libuv/include \
|
||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/libuv/include \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt
|
-Ideps/xopt \
|
||||||
|
-Werror
|
||||||
|
|
||||||
BASE64C_SOURCES = deps/base64c/src/base64c.c
|
BASE64C_SOURCES := deps/base64c/src/base64c.c
|
||||||
BASE64C_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(BASE64C_SOURCES))
|
BASE64C_OBJS := $(call get_objs,BASE64C_SOURCES)
|
||||||
$(BASE64C_OBJS): CFLAGS += \
|
$(BASE64C_OBJS): CFLAGS += \
|
||||||
-Wno-sign-compare
|
-Wno-sign-compare
|
||||||
|
|
||||||
BLOWFISH_SOURCES = \
|
BLOWFISH_SOURCES := \
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
deps/crypt_blowfish/crypt_gensalt.c \
|
deps/crypt_blowfish/crypt_gensalt.c \
|
||||||
deps/crypt_blowfish/wrapper.c
|
deps/crypt_blowfish/wrapper.c
|
||||||
BLOWFISH_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(BLOWFISH_SOURCES))
|
BLOWFISH_SOURCES_win = \
|
||||||
|
deps/crypt_blowfish/x86.S
|
||||||
|
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
|
||||||
|
|
||||||
UV_SOURCES = \
|
UV_SOURCES := \
|
||||||
deps/libuv/src/fs-poll.c \
|
deps/libuv/src/fs-poll.c \
|
||||||
deps/libuv/src/idna.c \
|
deps/libuv/src/idna.c \
|
||||||
deps/libuv/src/inet.c \
|
deps/libuv/src/inet.c \
|
||||||
deps/libuv/src/random.c \
|
deps/libuv/src/random.c \
|
||||||
deps/libuv/src/strscpy.c \
|
deps/libuv/src/strscpy.c \
|
||||||
|
deps/libuv/src/strtok.c \
|
||||||
deps/libuv/src/threadpool.c \
|
deps/libuv/src/threadpool.c \
|
||||||
deps/libuv/src/timer.c \
|
deps/libuv/src/timer.c \
|
||||||
|
deps/libuv/src/uv-common.c \
|
||||||
|
deps/libuv/src/uv-data-getter-setters.c \
|
||||||
|
deps/libuv/src/version.c
|
||||||
|
UV_SOURCES_unix := \
|
||||||
deps/libuv/src/unix/async.c \
|
deps/libuv/src/unix/async.c \
|
||||||
deps/libuv/src/unix/core.c \
|
deps/libuv/src/unix/core.c \
|
||||||
deps/libuv/src/unix/dl.c \
|
deps/libuv/src/unix/dl.c \
|
||||||
|
deps/libuv/src/unix/epoll.c \
|
||||||
deps/libuv/src/unix/fs.c \
|
deps/libuv/src/unix/fs.c \
|
||||||
deps/libuv/src/unix/getaddrinfo.c \
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
deps/libuv/src/unix/getnameinfo.c \
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
@ -79,29 +104,106 @@ UV_SOURCES = \
|
|||||||
deps/libuv/src/unix/tcp.c \
|
deps/libuv/src/unix/tcp.c \
|
||||||
deps/libuv/src/unix/thread.c \
|
deps/libuv/src/unix/thread.c \
|
||||||
deps/libuv/src/unix/tty.c \
|
deps/libuv/src/unix/tty.c \
|
||||||
deps/libuv/src/unix/udp.c \
|
deps/libuv/src/unix/udp.c
|
||||||
deps/libuv/src/uv-common.c \
|
UV_SOURCES_win := \
|
||||||
deps/libuv/src/uv-data-getter-setters.c \
|
deps/libuv/src/win/async.c \
|
||||||
deps/libuv/src/version.c
|
deps/libuv/src/win/core.c \
|
||||||
UV_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(UV_SOURCES))
|
deps/libuv/src/win/detect-wakeup.c \
|
||||||
|
deps/libuv/src/win/dl.c \
|
||||||
|
deps/libuv/src/win/error.c \
|
||||||
|
deps/libuv/src/win/fs-event.c \
|
||||||
|
deps/libuv/src/win/fs.c \
|
||||||
|
deps/libuv/src/win/getaddrinfo.c \
|
||||||
|
deps/libuv/src/win/getnameinfo.c \
|
||||||
|
deps/libuv/src/win/handle.c \
|
||||||
|
deps/libuv/src/win/loop-watcher.c \
|
||||||
|
deps/libuv/src/win/pipe.c \
|
||||||
|
deps/libuv/src/win/poll.c \
|
||||||
|
deps/libuv/src/win/process-stdio.c \
|
||||||
|
deps/libuv/src/win/process.c \
|
||||||
|
deps/libuv/src/win/signal.c \
|
||||||
|
deps/libuv/src/win/snprintf.c \
|
||||||
|
deps/libuv/src/win/stream.c \
|
||||||
|
deps/libuv/src/win/tcp.c \
|
||||||
|
deps/libuv/src/win/thread.c \
|
||||||
|
deps/libuv/src/win/tty.c \
|
||||||
|
deps/libuv/src/win/udp.c \
|
||||||
|
deps/libuv/src/win/util.c \
|
||||||
|
deps/libuv/src/win/winapi.c \
|
||||||
|
deps/libuv/src/win/winsock.c
|
||||||
|
UV_OBJS := $(call get_objs,UV_SOURCES)
|
||||||
$(UV_OBJS): CFLAGS += \
|
$(UV_OBJS): CFLAGS += \
|
||||||
-Ideps/libuv/include \
|
-Ideps/libuv/include \
|
||||||
-Ideps/libuv/src \
|
-Ideps/libuv/src \
|
||||||
-Wno-unused-but-set-variable \
|
-Wno-unused-but-set-variable \
|
||||||
-Wno-incompatible-pointer-types \
|
-Wno-incompatible-pointer-types \
|
||||||
-Wno-sign-compare \
|
-Wno-sign-compare \
|
||||||
-D_GNU_SOURCE \
|
-Wno-unused-variable \
|
||||||
|
-Wno-dangling-pointer \
|
||||||
|
-Wno-maybe-uninitialized \
|
||||||
|
-D_GNU_SOURCE
|
||||||
|
|
||||||
SQLITE_SOURCES = deps/sqlite/sqlite3.c
|
SODIUM_SOURCES := \
|
||||||
SQLITE_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(SQLITE_SOURCES))
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_verify/sodium/verify.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/core.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/runtime.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/utils.c
|
||||||
|
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
||||||
|
$(SODIUM_OBJS): CFLAGS += \
|
||||||
|
-DCONFIGURED=1 \
|
||||||
|
-DMINIMAL=1 \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable \
|
||||||
|
-Wno-type-limits \
|
||||||
|
-Wno-unknown-pragmas \
|
||||||
|
-Ideps/libsodium/src/libsodium/include/sodium
|
||||||
|
|
||||||
|
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||||
|
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||||
$(SQLITE_OBJS): CFLAGS += \
|
$(SQLITE_OBJS): CFLAGS += \
|
||||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||||
|
-DSQLITE_ENABLE_FTS5 \
|
||||||
-DSQLITE_ENABLE_JSON1 \
|
-DSQLITE_ENABLE_JSON1 \
|
||||||
-DSQLITE_MAX_LENGTH=5242880 \
|
-DSQLITE_MAX_LENGTH=5242880 \
|
||||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||||
-DSQLITE_MAX_COLUMN=100 \
|
-DSQLITE_MAX_COLUMN=100 \
|
||||||
-DSQLITE_MAX_EXPR_DEPTH=20 \
|
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||||
-DSQLITE_MAX_COMPOUND_SELECT=3 \
|
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||||
-DSQLITE_MAX_ATTACHED=0 \
|
-DSQLITE_MAX_ATTACHED=0 \
|
||||||
@ -109,61 +211,117 @@ $(SQLITE_OBJS): CFLAGS += \
|
|||||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||||
-DSQLITE_SECURE_DELETE \
|
-DSQLITE_SECURE_DELETE \
|
||||||
-Wno-implicit-fallthrough
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-unused-but-set-variable
|
||||||
|
|
||||||
XOPT_SOURCES = deps/xopt/xopt.c
|
XOPT_SOURCES := deps/xopt/xopt.c
|
||||||
XOPT_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(XOPT_SOURCES))
|
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
||||||
|
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
||||||
|
-DHAVE_SNPRINTF \
|
||||||
|
-DHAVE_VSNPRINTF \
|
||||||
|
-DHAVE_VASNPRINTF \
|
||||||
|
-DHAVE_VASPRINTF \
|
||||||
|
-Dvsnprintf=rpl_vsnprintf
|
||||||
|
|
||||||
QUICKJS_SOURCES = \
|
QUICKJS_SOURCES := \
|
||||||
deps/quickjs/cutils.c \
|
deps/quickjs/cutils.c \
|
||||||
deps/quickjs/libbf.c \
|
deps/quickjs/libbf.c \
|
||||||
deps/quickjs/libregexp.c \
|
deps/quickjs/libregexp.c \
|
||||||
deps/quickjs/libunicode.c \
|
deps/quickjs/libunicode.c \
|
||||||
deps/quickjs/quickjs-libc.c \
|
deps/quickjs/quickjs-libc.c \
|
||||||
deps/quickjs/quickjs.c
|
deps/quickjs/quickjs.c
|
||||||
QUICKJS_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(QUICKJS_SOURCES))
|
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||||
$(QUICKJS_OBJS): CFLAGS += \
|
$(QUICKJS_OBJS): CFLAGS += \
|
||||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||||
|
-DCONFIG_BIGNUM \
|
||||||
-DDUMP_LEAKS \
|
-DDUMP_LEAKS \
|
||||||
-D_GNU_SOURCE \
|
-D_GNU_SOURCE \
|
||||||
-Wno-sign-compare \
|
-Wno-sign-compare \
|
||||||
-Wno-implicit-fallthrough \
|
-Wno-implicit-fallthrough \
|
||||||
-Wno-unused-variable \
|
-Wno-unused-variable \
|
||||||
-Wno-unused-but-set-variable
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-enum-conversion
|
||||||
|
|
||||||
APP_LDFLAGS = \
|
LIBBACKTRACE_SOURCES := \
|
||||||
$(COMMON_LDFLAGS) \
|
deps/libbacktrace/atomic.c \
|
||||||
$(LDFLAGS) \
|
deps/libbacktrace/backtrace.c \
|
||||||
|
deps/libbacktrace/dwarf.c \
|
||||||
|
deps/libbacktrace/fileline.c \
|
||||||
|
deps/libbacktrace/print.c \
|
||||||
|
deps/libbacktrace/simple.c \
|
||||||
|
deps/libbacktrace/sort.c \
|
||||||
|
deps/libbacktrace/state.c
|
||||||
|
LIBBACKTRACE_SOURCES_unix := \
|
||||||
|
deps/libbacktrace/elf.c \
|
||||||
|
deps/libbacktrace/mmap.c \
|
||||||
|
deps/libbacktrace/mmapio.c \
|
||||||
|
deps/libbacktrace/posix.c
|
||||||
|
LIBBACKTRACE_SOURCES_win := \
|
||||||
|
deps/libbacktrace/alloc.c \
|
||||||
|
deps/libbacktrace/pecoff.c \
|
||||||
|
deps/libbacktrace/posix.c \
|
||||||
|
deps/libbacktrace/read.c
|
||||||
|
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
|
||||||
|
$(LIBBACKTRACE_OBJS): CFLAGS += \
|
||||||
|
-Ideps/libbacktrace_config \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-maybe-initialized \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-DBACKTRACE_ELF_SIZE=64
|
||||||
|
|
||||||
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
|
-lm
|
||||||
|
debug release: LDFLAGS += \
|
||||||
-ldl \
|
-ldl \
|
||||||
-lm \
|
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto \
|
-lcrypto
|
||||||
-lsodium
|
windebug winrelease: LDFLAGS += \
|
||||||
|
-lwsock32 \
|
||||||
|
-lws2_32 \
|
||||||
|
-lkernel32 \
|
||||||
|
-liphlpapi \
|
||||||
|
-luserenv
|
||||||
|
|
||||||
DEFAULT_TARGET = $(APP_BIN)
|
unix: debug release
|
||||||
all: $(DEFAULT_TARGET)
|
win: windebug winrelease
|
||||||
.PHONY: all
|
all: $(BUILD_TYPES)
|
||||||
|
.PHONY: all win unix
|
||||||
|
|
||||||
ALL_APP_OBJS = \
|
ALL_APP_OBJS := \
|
||||||
$(APP_OBJS) \
|
$(APP_OBJS) \
|
||||||
$(BASE64C_OBJS) \
|
$(BASE64C_OBJS) \
|
||||||
$(BLOWFISH_OBJS) \
|
$(BLOWFISH_OBJS) \
|
||||||
$(UV_OBJS) \
|
$(LIBBACKTRACE_OBJS) \
|
||||||
$(SQLITE_OBJS) \
|
|
||||||
$(QUICKJS_OBJS) \
|
$(QUICKJS_OBJS) \
|
||||||
|
$(SODIUM_OBJS) \
|
||||||
|
$(SQLITE_OBJS) \
|
||||||
|
$(UV_OBJS) \
|
||||||
$(XOPT_OBJS)
|
$(XOPT_OBJS)
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
-include $(DEPS)
|
-include $(DEPS)
|
||||||
|
|
||||||
$(APP_BIN): $(ALL_APP_OBJS)
|
define build_rules
|
||||||
$(CC) -o $@ $^ $(APP_LDFLAGS)
|
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
||||||
|
.PHONY: $(1)
|
||||||
|
|
||||||
$(BUILD_DIR)/%.o: %.c
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
@mkdir -p $(dir $@)
|
@echo [link] $$@
|
||||||
@echo [c] $@
|
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||||
@$(CC) $(COMMON_CFLAGS) $(CFLAGS) -c $< -o $@
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo [c] $$@
|
||||||
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.S
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo [as] $$@
|
||||||
|
@$$(AS) -c $$< -o $$@
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
36
README.md
36
README.md
@ -1,26 +1,36 @@
|
|||||||
# Tilde Friends
|
# Tilde Friends
|
||||||
Tilde Friends is a program that aims to securely host and share pure JavaScript web applications.
|
Tilde Friends is a tool for making and sharing.
|
||||||
|
|
||||||
|
It is both a peer-to-peer social network client, participating in Secure
|
||||||
|
Scuttlebutt, as well as a platform for writing and running web applications.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
1. Make it easy and fun to run all sorts of web applications.
|
1. Make it easy and fun to run all sorts of web applications.
|
||||||
2. Provide a security model that is easy to understand and protects your data.
|
2. Provide security that is easy to understand and protects your data.
|
||||||
3. Make creating and sharing web applications accessible to anyone with a browser.
|
3. Make creating and sharing web applications accessible to anyone with a
|
||||||
|
browser.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
1. Requires libsodium and openssl. Other dependencies are kept up to date in the tree.
|
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
||||||
2. To build, run `make` or `make DEBUG=1`. An executable will be generated in a subdirectory of `out/`.
|
are kept up to date in the tree.
|
||||||
|
2. To build, run `make debug` or `make release`. An executable will be
|
||||||
|
generated in a subdirectory of `out/`.
|
||||||
|
3. `make windebug` or `make winrelease` will generate a windows executable
|
||||||
|
which might work.
|
||||||
|
4. To build in docker, `docker build .`.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
This is only just starting to show some signs of beginning to work as intended. Set expectations low.
|
By default, running the built `tildefriends` executable will start a web server
|
||||||
|
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
|
|
||||||
Running the built `tildefriends` executable will start a web server at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
The first user to create an account and log in will be granted administrative
|
||||||
|
privileges. Further administration can be done at
|
||||||
The first user to create an account and log in will be granted administrative privileges. Everything can be managed entirely from the web interface.
|
<http://localhost:12345/~core/admin/`>.
|
||||||
|
|
||||||
Some starter apps can be installed by running `tildefriends import -u cory`. Hint: `~cory/docs/` and `~cory/index/`.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
There are the very beginnings of developer documentation in `apps/cory/docs/` that can be read in-place or in-browser by running `tildefriends import -u cory` and then visiting <http://localhost:12345/~cory/docs/>.
|
There are the very beginnings of developer documentation in `apps/cory/docs/`
|
||||||
|
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
All code unless otherwise noted in [COPYING](https://www.unprompted.com/projects/browser/projects/tildefriends/trunk/COPYING) is provided under the [Affero GPL 3.0](https://www.unprompted.com/projects/browser/projects/tildefriends/trunk/LICENSE) license.
|
All code unless otherwise noted in is provided under the
|
||||||
|
[MIT](https://opensource.org/licenses/MIT) license.
|
||||||
|
1
apps/cory/admin.json
Normal file
1
apps/cory/admin.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&uhGJsy5+qBgOgEgMqCTDasK+C+GWGptHKfPiAsD5eGA=.sha256","index.html":"&D3JwdPXy/QsLXkmwNDrBFXdzxfqO1/JGxfqEArnS5v4=.sha256","lit.min.js":"&3FfrVflmGr0n4lvN0GriN1Qz1lEw31SbZxRSJrcXR28=.sha256","script.js":"&TZ2ymD6cFVUjQleGcDslt8apjp7k3xLlfv2F8rQVM4I=.sha256"}}
|
22
apps/cory/admin/app.js
Normal file
22
apps/cory/admin/app.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
tfrpc.register(function delete_user(user) {
|
||||||
|
return core.deleteUser(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(function global_settings_set(key, value) {
|
||||||
|
return core.globalSettingsSet(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let data = {
|
||||||
|
users: {},
|
||||||
|
granted: await core.allPermissionsGranted(),
|
||||||
|
settings: await core.globalSettingsDescriptions(),
|
||||||
|
};
|
||||||
|
for (let user of await core.users()) {
|
||||||
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
|
}
|
||||||
|
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
||||||
|
}
|
||||||
|
main();
|
10
apps/cory/admin/index.html
Normal file
10
apps/cory/admin/index.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script>const g_data = $data;</script>
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff">
|
||||||
|
<h1>Tilde Friends Administration</h1>
|
||||||
|
</body>
|
||||||
|
<script type="module" src="script.js"></script>
|
||||||
|
</html>
|
13
apps/cory/admin/lit.min.js
vendored
Normal file
13
apps/cory/admin/lit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
78
apps/cory/admin/script.js
Normal file
78
apps/cory/admin/script.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {html, render} from './lit.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
function delete_user(user) {
|
||||||
|
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||||
|
tfrpc.rpc.delete_user(user).then(function() {
|
||||||
|
alert(`User "${user}" deleted successfully.`);
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function global_settings_set(key, value) {
|
||||||
|
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
||||||
|
alert(`Set "${key}" to "${value}".`);
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
const permission_template = (permission) =>
|
||||||
|
html` <code>${permission}</code>`;
|
||||||
|
function input_template(key, description) {
|
||||||
|
if (description.type === 'boolean') {
|
||||||
|
return html`
|
||||||
|
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||||
|
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
|
||||||
|
<div style="grid-column: 3">
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
|
||||||
|
<span>${description.description}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (description.type === 'textarea') {
|
||||||
|
return html`
|
||||||
|
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||||
|
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
|
||||||
|
<div style="grid-column: 3">
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
||||||
|
<span>${description.description}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
||||||
|
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
|
||||||
|
<div style="grid-column: 3">
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
||||||
|
<span>${description.description}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const user_template = (user, permissions) => html`
|
||||||
|
<li>
|
||||||
|
<button @click=${(e) => delete_user(user)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
${user}:
|
||||||
|
${permissions.map(x => permission_template(x))}
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
const users_template = (users) =>
|
||||||
|
html`<h2>Users</h2>
|
||||||
|
<ul>
|
||||||
|
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
||||||
|
</ul>`;
|
||||||
|
const page_template = (data) =>
|
||||||
|
html`<div>
|
||||||
|
<h2>Global Settings</h2>
|
||||||
|
<div style="display: grid">
|
||||||
|
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||||
|
</div>
|
||||||
|
${users_template(data.users)}
|
||||||
|
</div>`;
|
||||||
|
render(page_template(g_data), document.body);
|
||||||
|
});
|
1
apps/cory/api.json
Normal file
1
apps/cory/api.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&p35JmopfHf8hFh3Y9x6LrIxiUwaJZ5Nabzi2sVXpKoo=.sha256"}}
|
11
apps/cory/api/app.js
Normal file
11
apps/cory/api/app.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
var global = Function('return this')();
|
||||||
|
function treeify(o) {
|
||||||
|
if (typeof(o) == 'object') {
|
||||||
|
return Object.fromEntries(Object.keys(o).map(x => [x, treeify(o[x])]));
|
||||||
|
} else if (typeof(o) == 'function') {
|
||||||
|
return 'function';
|
||||||
|
} else if (typeof(o) == 'string' || typeof(o) == 'number') {
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.setDocument(`<pre style="color:#fff">${JSON.stringify(treeify(global), null, 2)}</pre>`);
|
1
apps/cory/apps.json
Normal file
1
apps/cory/apps.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&qEJDfZ43KazIxiZl8OCKb2uaDOsPkxnIohEzQ1LLFpg=.sha256"}}
|
31
apps/cory/apps/app.js
Normal file
31
apps/cory/apps/app.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
async function main() {
|
||||||
|
var apps = await core.apps();
|
||||||
|
var core_apps = await core.apps('core');
|
||||||
|
var doc = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background: #888">
|
||||||
|
<h1>Apps</h1>
|
||||||
|
<ul id="apps"></ul>
|
||||||
|
<h1>Core Apps</h1>
|
||||||
|
<ul id="core_apps"></ul>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
function populate_apps(id, name, apps) {
|
||||||
|
var list = document.getElementById(id);
|
||||||
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
var li = list.appendChild(document.createElement('li'));
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.innerText = app;
|
||||||
|
a.href = '/~' + name + '/' + app + '/';
|
||||||
|
a.target = '_top';
|
||||||
|
li.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
|
</script>
|
||||||
|
</html>`
|
||||||
|
app.setDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
1
apps/cory/db.json
Normal file
1
apps/cory/db.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&V5o5IM9/OUyIsVkjkMW/X0i/tflQOSVJuJBmHdMT9aM=.sha256"}}
|
70
apps/cory/db/app.js
Normal file
70
apps/cory/db/app.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
async function database_list() {
|
||||||
|
var dbs = await databases();
|
||||||
|
var doc = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background: #888">
|
||||||
|
<h1>Databases</h1>
|
||||||
|
<ul id="dbs"></ul>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
function populate_dbs(id, dbs) {
|
||||||
|
var list = document.getElementById(id);
|
||||||
|
for (let db of dbs) {
|
||||||
|
var li = list.appendChild(document.createElement('li'));
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.innerText = db;
|
||||||
|
a.href = './#' + db;
|
||||||
|
a.target = '_top';
|
||||||
|
li.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||||
|
</script>
|
||||||
|
</html>`
|
||||||
|
app.setDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function key_list(db) {
|
||||||
|
let keys = await db.getAll();
|
||||||
|
let object = {};
|
||||||
|
for (let key of keys) {
|
||||||
|
object[key] = await db.get(key);
|
||||||
|
}
|
||||||
|
let doc = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body style="background: #888">
|
||||||
|
<a href="#" target="_top">back</a>
|
||||||
|
<h1>Keys</h1>
|
||||||
|
<ul id="keys"></ul>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
function populate_dbs(id, keys) {
|
||||||
|
var list = document.getElementById(id);
|
||||||
|
for (let [key, value] of Object.entries(keys)) {
|
||||||
|
var li = list.appendChild(document.createElement('li'));
|
||||||
|
li.innerText = key + ' = ' + value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
populate_dbs('keys', ${JSON.stringify(object)});
|
||||||
|
</script>
|
||||||
|
</html>`
|
||||||
|
app.setDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.register('message', async function(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
let hash = message.hash.substring(1);
|
||||||
|
if (hash.startsWith(':shared:')) {
|
||||||
|
let parts = hash.split(':');
|
||||||
|
let packageName = parts[3];
|
||||||
|
let key = parts.slice(4).join(':');
|
||||||
|
key_list(await my_shared_database(packageName, key));
|
||||||
|
} else if (hash.length) {
|
||||||
|
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||||
|
} else {
|
||||||
|
database_list();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
database_list();
|
@ -1 +1 @@
|
|||||||
{"type":"tildefriends-app","files":{"app.js":"&WCq6ssQedT5denXPXlz2BswPD6hmt++EmWIMIDUMurA=.sha256","index.md":"&Lr7IXs8osbmWz6SDsGTQCiybbxkbWSK2MrUcXMzgqTs=.sha256","todo.md":"&XrOJ3D5YMTN+j+0hJgLLy7Y61B6Z14ebv+60ee+N37I=.sha256","structure.md":"&xRhQ4Mpom1Idskum07osbBQYcYWroH0sELQBkQHrOMg=.sha256","purpose.md":"&c0/YqFhXC0X3DqiEo55NqzI5wq0VTw6cVZTf/gAWS3w=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256"}}
|
{"type":"tildefriends-app","files":{"app.js":"&WEvJYebSMi5d2eXgUwJJmvR/Q4slFg3zHYB8Q2mXJII=.sha256","index.md":"&Pi0NTJn9/w76yIUKqRRuSvUPSpqkxdYynmjeOBbF3K8=.sha256","todo.md":"&d8Kq8yuOn8SL3tJVy9BiDXHAe/jverpBj5AMLWLtmFM=.sha256","structure.md":"&T+CBfT9XP6ooKFvD1ZCI9hsutqsNIamfBxtAho0HtlU=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256","ssb.md":"&ouqT3XzTGfBNpOP/uEdOw7K1F9BeLZgQCx24XTvhyXU=.sha256"}}
|
File diff suppressed because one or more lines are too long
17
apps/cory/docs/id_refactor.md
Normal file
17
apps/cory/docs/id_refactor.md
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# ID Refactor
|
||||||
|
[Back to index](#index)
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
- no way to get private key in javascript
|
||||||
|
- ssb.c syncs/broadcasts/... efficiently for everybody
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
- separate table to discourage leakage
|
||||||
|
- `CREATE TABLE identities (user TEXT, public TEXT, secret TEXT);`
|
||||||
|
|
||||||
|
## API
|
||||||
|
- `ssb.createIdentity()` -> `id`
|
||||||
|
- `ssb.getIdentities()` => `[id, ...]`
|
||||||
|
- `ssb.deleteIdentity(id)`
|
||||||
|
- `ssb.post(id, ...)`
|
||||||
|
- `ssb.appendMessage(id, ...)`
|
@ -5,7 +5,7 @@ network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
|||||||
augmenting it with a way to safely and securely write, share,
|
augmenting it with a way to safely and securely write, share,
|
||||||
and run code.
|
and run code.
|
||||||
|
|
||||||
- [Purpose](#purpose)
|
- [Secure Scuttlebutt from Scratch](#ssb)
|
||||||
- [Structure](#structure)
|
- [Structure](#structure)
|
||||||
- [Guide](#guide)
|
- [Guide](#guide)
|
||||||
- [TODO](#todo)
|
- [TODO](#todo)
|
4
apps/cory/docs/markdeep.min.js
vendored
4
apps/cory/docs/markdeep.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,24 +0,0 @@
|
|||||||
# Tilde Friends Purpose
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
## Beliefs
|
|
||||||
1. The web is the universal virtual machine.
|
|
||||||
- It is here, ready to be used from your desktop, laptop, smart phone,
|
|
||||||
tablet, game console, and smart TV.
|
|
||||||
- It is not ideal, but it is the best we have right now,
|
|
||||||
and all signs point to it continuing to improve, at least
|
|
||||||
in terms of features, security, and device support.
|
|
||||||
2. Distributed is superior to centralized.
|
|
||||||
- Distributed services don't need ads.
|
|
||||||
- Distributed services can't be acquired by evil corporations.
|
|
||||||
- Distributed services respect the user's privacy.
|
|
||||||
- Distributed services respect the user.
|
|
||||||
3. Offline-first is superior to online-only.
|
|
||||||
- The internet goes down sometimes. Applications should continue
|
|
||||||
to work.
|
|
||||||
3. Making and sharing code should be easy.
|
|
||||||
- Cloning your repository, installing dev tools, running a
|
|
||||||
docker image, or fighting with dependencies is *not* easy.
|
|
||||||
- If you see a thing in a web browser, you should be able to click
|
|
||||||
`edit`, make a change, save, and see the result.
|
|
||||||
[Wikipedia](https://www.wikipedia.org/) is easy.
|
|
37
apps/cory/docs/ssb.md
Normal file
37
apps/cory/docs/ssb.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Secure Scuttlebutt from Scratch
|
||||||
|
[Back to index](#index)
|
||||||
|
|
||||||
|
This aims to be the missing reference for those who wish to create a Secure
|
||||||
|
Scuttlebutt client from scratch.
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
A good way to get started is to participate in local network discovery with a known working
|
||||||
|
client on the same network. The
|
||||||
|
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
|
||||||
|
is a good start, here, with a few things to note:
|
||||||
|
|
||||||
|
1. Some clients advertise multiple addresses separated by semicolons (`;`).
|
||||||
|
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
|
||||||
|
IPv4 addresses.
|
||||||
|
|
||||||
|
So be prepared to accept variations.
|
||||||
|
|
||||||
|
## Secret Handshake, Box Stream, and RPC Protocol
|
||||||
|
Now that two clients are aware of eachother, they need to complete a secret handshake.
|
||||||
|
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
|
||||||
|
is once again a good reference.
|
||||||
|
|
||||||
|
The box stream and RPC protocol can both be implemented from the
|
||||||
|
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
|
||||||
|
without surprises.
|
||||||
|
|
||||||
|
## Synchronizing Data
|
||||||
|
So now you're discovering other clients on the local network, connecting, performing
|
||||||
|
a secret handshake, and making remote procedure calls over box streams. The next step
|
||||||
|
is to start synchronizing feeds over the network. The goal, after all, is to author
|
||||||
|
messages in your local append-only log and have them show up in distant clients, or
|
||||||
|
vice versa.
|
||||||
|
|
||||||
|
## References
|
||||||
|
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
|
||||||
|
* [https://dev.planetary.social/](https://dev.planetary.social/)
|
@ -57,10 +57,8 @@ browser. This process has a custom RPC connection to the core process
|
|||||||
which holds the WebSocket connection to the browser.
|
which holds the WebSocket connection to the browser.
|
||||||
|
|
||||||
The custom RPC communication between the sandbox process and the core
|
The custom RPC communication between the sandbox process and the core
|
||||||
process facilitates calling functions asynchronously. Calling a remote
|
process facilitates passing and calling functions remotely. Calling a
|
||||||
function (ie. a function in another process) returns a `Promise`. In
|
function in another process returns a `Promise`.
|
||||||
addition, any functions passed in either direction are serialized in
|
|
||||||
such a way that they can be called remotely.
|
|
||||||
|
|
||||||
An application will typically call `app.setDocument()` at startup to
|
An application will typically call `app.setDocument()` at startup to
|
||||||
populate the app's iframe in the web browser with its own client web
|
populate the app's iframe in the web browser with its own client web
|
||||||
|
@ -1,48 +1,64 @@
|
|||||||
# Tilde Friends TODO
|
# Tilde Friends TODO
|
||||||
[Back to index](#index)
|
[Back to index](#index)
|
||||||
|
|
||||||
## MVP
|
## MVP3
|
||||||
- release
|
- Sync status (problem feeds, messages/seconds stats, ...)
|
||||||
- blog
|
- app: wiki
|
||||||
- update COPYING
|
- app: public blog
|
||||||
- update README
|
- app: build archive
|
||||||
- auto-populate data on initial launch
|
- app: todo
|
||||||
- audit + document API exposed to apps
|
- Content-Disposition: download
|
||||||
- ssb core
|
- remove SSB credentials
|
||||||
- good refresh
|
- export SSB credentials
|
||||||
- disconnect all current connections and reset reconnect timers?
|
- initial: better empty news screen
|
||||||
- reload the page
|
- initial: remembered wrong user across login/logout
|
||||||
- live updates
|
- initial: bad experience when following nobody
|
||||||
- createHistoryStream for every account followed from local accounts
|
- make a cool independent app
|
||||||
- apps
|
- indicate when workspace differs from installed
|
||||||
- app messages
|
- / => Something good.
|
||||||
- installable apps
|
- update docs
|
||||||
- web interface
|
- audit + document API exposed to apps
|
||||||
- live updates
|
- sqlStream => sqlExec or something
|
||||||
- strip out unnecessary things?
|
- fix weird HTTP warnings
|
||||||
- more raw views until it's more functional?
|
- ssb from child process?
|
||||||
|
- channels
|
||||||
|
- image downsample
|
||||||
|
- placeholder/missing images
|
||||||
|
- no denial of service
|
||||||
|
- package standalone executable
|
||||||
|
- blob_wants 2.0
|
||||||
|
- editor without app iframe
|
||||||
|
- sequence_before_author -> flags
|
||||||
|
- linkify ssb: links
|
||||||
|
|
||||||
|
## MVP2
|
||||||
|
- connections 2.0
|
||||||
|
- make a better connections API
|
||||||
|
|
||||||
|
## Maybe Done
|
||||||
|
- update README
|
||||||
|
- administrators config
|
||||||
|
- apps name characters
|
||||||
|
- initial: can't switch to account when there is only one
|
||||||
|
- get tarball under 5MB
|
||||||
|
- rooms
|
||||||
|
- initial: doesn't refresh when create identity
|
||||||
|
- tf account timeout why
|
||||||
|
- ssb don't overflow boxes
|
||||||
|
- jwt for session tokens
|
||||||
|
- linkify https://...
|
||||||
|
- emoji reaction picker
|
||||||
|
- expose loads of stats
|
||||||
|
- confirm posting all new messages
|
||||||
|
- multiple identities per user, in database
|
||||||
|
- auto-populate data on initial launch
|
||||||
|
- make the docker image good / test it / use it
|
||||||
|
- leaking imports / exports
|
||||||
|
- file upload widget
|
||||||
|
- keep working on good error feedback
|
||||||
|
- build for windows
|
||||||
|
- installable apps (bring back an app message?)
|
||||||
|
|
||||||
## Done
|
## Done
|
||||||
- likely classes of script errors
|
- update LICENSE
|
||||||
- tf core
|
- logging to browser
|
||||||
- good error feedback
|
|
||||||
- markdeep demo
|
|
||||||
- send blobs
|
|
||||||
|
|
||||||
## Later
|
|
||||||
- DB migration
|
|
||||||
- stop using CDNs
|
|
||||||
- collect loads of stats
|
|
||||||
- faster save - parallel / don't save unmodified
|
|
||||||
- test likely denials of service
|
|
||||||
- package standalone executable
|
|
||||||
- ideas
|
|
||||||
- visualizations / analysis of gps data
|
|
||||||
- good web interface for managing connections
|
|
||||||
- identity
|
|
||||||
- multiple identities
|
|
||||||
- tie identities to TF login accounts
|
|
||||||
- tf account timeout why
|
|
||||||
- make some demo apps
|
|
||||||
- rock paper scissors, somehow
|
|
||||||
- don't resave files that didn't change
|
|
1
apps/cory/follow.json
Normal file
1
apps/cory/follow.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&3d9ABFgRwQvWsYbFv/rzimtnLDnVrWlGtdw7serFIGw=.sha256"}}
|
159
apps/cory/follow/app.js
Normal file
159
apps/cory/follow/app.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
var g_following_cache = {};
|
||||||
|
var g_following_deep_cache = {};
|
||||||
|
var g_about_cache = {};
|
||||||
|
|
||||||
|
async function following(db, id) {
|
||||||
|
if (g_following_cache[id]) {
|
||||||
|
return g_following_cache[id];
|
||||||
|
}
|
||||||
|
var o = await db.get(id + ":following");
|
||||||
|
const k_version = 5;
|
||||||
|
var f = o ? JSON.parse(o) : o;
|
||||||
|
if (!f || f.version != k_version) {
|
||||||
|
f = {users: [], sequence: 0, version: k_version};
|
||||||
|
}
|
||||||
|
f.users = new Set(f.users);
|
||||||
|
await ssb.sqlStream(
|
||||||
|
"SELECT "+
|
||||||
|
" sequence, "+
|
||||||
|
" json_extract(content, '$.contact') AS contact, "+
|
||||||
|
" json_extract(content, '$.following') AS following "+
|
||||||
|
"FROM messages "+
|
||||||
|
"WHERE "+
|
||||||
|
" author = ?1 AND "+
|
||||||
|
" sequence > ?2 AND "+
|
||||||
|
" json_extract(content, '$.type') = 'contact' "+
|
||||||
|
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
||||||
|
"ORDER BY sequence",
|
||||||
|
[id, f.sequence],
|
||||||
|
function(row) {
|
||||||
|
if (row.following) {
|
||||||
|
f.users.add(row.contact);
|
||||||
|
} else {
|
||||||
|
f.users.delete(row.contact);
|
||||||
|
}
|
||||||
|
f.sequence = row.sequence;
|
||||||
|
});
|
||||||
|
var as_set = f.users;
|
||||||
|
f.users = Array.from(f.users).sort();
|
||||||
|
var j = JSON.stringify(f);
|
||||||
|
if (o != j) {
|
||||||
|
await db.set(id + ":following", j);
|
||||||
|
}
|
||||||
|
f.users = as_set;
|
||||||
|
g_following_cache[id] = f.users;
|
||||||
|
return f.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function followingDeep(db, seed_ids, depth) {
|
||||||
|
if (depth <= 0) {
|
||||||
|
return seed_ids;
|
||||||
|
}
|
||||||
|
var key = JSON.stringify([seed_ids, depth]);
|
||||||
|
if (g_following_deep_cache[key]) {
|
||||||
|
return g_following_deep_cache[key];
|
||||||
|
}
|
||||||
|
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
|
||||||
|
var ids = [].concat(...f);
|
||||||
|
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
||||||
|
x = [...new Set([].concat(...x, ...seed_ids))].sort();
|
||||||
|
g_following_deep_cache[key] = x;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAbout(db, id) {
|
||||||
|
if (g_about_cache[id]) {
|
||||||
|
return g_about_cache[id];
|
||||||
|
}
|
||||||
|
var o = await db.get(id + ":about");
|
||||||
|
const k_version = 4;
|
||||||
|
var f = o ? JSON.parse(o) : o;
|
||||||
|
if (!f || f.version != k_version) {
|
||||||
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
|
}
|
||||||
|
await ssb.sqlStream(
|
||||||
|
"SELECT "+
|
||||||
|
" sequence, "+
|
||||||
|
" content "+
|
||||||
|
"FROM messages "+
|
||||||
|
"WHERE "+
|
||||||
|
" author = ?1 AND "+
|
||||||
|
" sequence > ?2 AND "+
|
||||||
|
" json_extract(content, '$.type') = 'about' AND "+
|
||||||
|
" json_extract(content, '$.about') = ?1 "+
|
||||||
|
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
||||||
|
"ORDER BY sequence",
|
||||||
|
[id, f.sequence],
|
||||||
|
function(row) {
|
||||||
|
f.sequence = row.sequence;
|
||||||
|
if (row.content) {
|
||||||
|
var about = {};
|
||||||
|
try {
|
||||||
|
about = JSON.parse(row.content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
delete about.about;
|
||||||
|
delete about.type;
|
||||||
|
f.about = Object.assign(f.about, about);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var j = JSON.stringify(f);
|
||||||
|
if (o != j) {
|
||||||
|
await db.set(id + ":about", j);
|
||||||
|
}
|
||||||
|
g_about_cache[id] = f.about;
|
||||||
|
return f.about;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSize(db, id) {
|
||||||
|
let size = 0;
|
||||||
|
await ssb.sqlStream(
|
||||||
|
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
||||||
|
[id],
|
||||||
|
function (row) {
|
||||||
|
size += row.size;
|
||||||
|
});
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function niceSize(bytes) {
|
||||||
|
let value = bytes;
|
||||||
|
let unit = 'B';
|
||||||
|
const k_units = ['kB', 'MB', 'GB', 'TB'];
|
||||||
|
for (let u of k_units) {
|
||||||
|
if (value >= 1024) {
|
||||||
|
value /= 1024;
|
||||||
|
unit = u;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTree(db, root, indent, depth) {
|
||||||
|
var f = await following(db, root);
|
||||||
|
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
|
||||||
|
if (depth > 0) {
|
||||||
|
for (let next of f) {
|
||||||
|
result += await buildTree(db, next, indent + ' ', depth - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||||
|
var db = await database('ssb');
|
||||||
|
var whoami = await ssb.getIdentities();
|
||||||
|
var tree = '';
|
||||||
|
for (let id of whoami) {
|
||||||
|
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
|
||||||
|
tree += await buildTree(db, id, '', 2);
|
||||||
|
}
|
||||||
|
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
@ -1 +0,0 @@
|
|||||||
{"type":"tildefriends-app","files":{"app.js":"&6uFJG2C0kZar1Aj+7p2/KzYEBXgmK/uJSt7aIJqenN4=.sha256","index.html":"&TFtniuUIVO7XeWCgwmqPAmuBzpGX6slxJQcPMEr+860=.sha256","vue-material.js":"&K5cdLqXYCENPak/TCINHQhyJhpS4G9DlZHGwoh/LF2g=.sha256"}}
|
|
@ -1,381 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const k_posts_max = 20;
|
|
||||||
const k_votes_max = 100;
|
|
||||||
|
|
||||||
async function following(db, id) {
|
|
||||||
var o = await db.get(id + ":following");
|
|
||||||
const k_version = 4;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {users: [], sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
f.users = new Set(f.users);
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" json_extract(content, '$.contact') AS contact, "+
|
|
||||||
" json_extract(content, '$.following') AS following "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" author = ?1 AND "+
|
|
||||||
" sequence > ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'contact' "+
|
|
||||||
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[id, f.sequence],
|
|
||||||
async function(row) {
|
|
||||||
if (row.following) {
|
|
||||||
f.users.add(row.contact);
|
|
||||||
} else {
|
|
||||||
f.users.delete(row.contact);
|
|
||||||
}
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
});
|
|
||||||
f.users = Array.from(f.users);
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":following", j);
|
|
||||||
}
|
|
||||||
return f.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function followingDeep(db, seed_ids, depth) {
|
|
||||||
if (depth <= 0) {
|
|
||||||
return seed_ids;
|
|
||||||
}
|
|
||||||
var f = await Promise.all(seed_ids.map(x => following(db, x)));
|
|
||||||
var ids = [].concat(...f);
|
|
||||||
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
|
||||||
x = [].concat(...x, ...seed_ids);
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function followers(db, id) {
|
|
||||||
var o = await db.get(id + ":followers");
|
|
||||||
const k_version = 2;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {users: [], rowid: 0, version: k_version};
|
|
||||||
}
|
|
||||||
f.users = new Set(f.users);
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" rowid, "+
|
|
||||||
" author AS contact, "+
|
|
||||||
" json_extract(content, '$.following') AS following "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" rowid > $1 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'contact' AND "+
|
|
||||||
" json_extract(content, '$.contact') = $2 "+
|
|
||||||
"UNION SELECT MAX(rowid) as rowid, NULL, NULL FROM messages "+
|
|
||||||
"ORDER BY rowid",
|
|
||||||
[f.rowid, id],
|
|
||||||
async function(row) {
|
|
||||||
if (row.following) {
|
|
||||||
f.users.add(row.contact);
|
|
||||||
} else {
|
|
||||||
f.users.delete(row.contact);
|
|
||||||
}
|
|
||||||
f.rowid = row.rowid;
|
|
||||||
});
|
|
||||||
f.users = Array.from(f.users);
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":followers", j);
|
|
||||||
}
|
|
||||||
return f.users;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendUser(db, id) {
|
|
||||||
return Promise.all([
|
|
||||||
following(db, id).then(async function(following) {
|
|
||||||
return app.postMessage({following: {id: id, users: following}});
|
|
||||||
}),
|
|
||||||
followers(db, id).then(async function(followers) {
|
|
||||||
return app.postMessage({followers: {id: id, users: followers}});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pubsByUser(db, id) {
|
|
||||||
var o = await db.get(id + ":pubs");
|
|
||||||
const k_version = 2;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {pubs: [], sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
f.pubs = Object.fromEntries(f.pubs.map(x => [JSON.stringify(x), x]));
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" json_extract(content, '$.address.host') AS host, "+
|
|
||||||
" json_extract(content, '$.address.port') AS port, "+
|
|
||||||
" json_extract(content, '$.address.key') AS key "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" sequence > ?1 AND "+
|
|
||||||
" author = ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'pub' "+
|
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL, NULL, NULL FROM messages WHERE author = ?2 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[f.sequence, id],
|
|
||||||
async function(row) {
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
if (row.host) {
|
|
||||||
row = {host: row.host, port: row.port, key: row.key};
|
|
||||||
f.pubs[JSON.stringify(row)] = row;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
f.pubs = Object.values(f.pubs);
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":pubs", j);
|
|
||||||
}
|
|
||||||
return f.pubs;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function visiblePubs(db, id) {
|
|
||||||
var ids = [id].concat(await following(db, id));
|
|
||||||
var pubs = {};
|
|
||||||
for (var follow of ids) {
|
|
||||||
var followPubs = await pubsByUser(db, follow);
|
|
||||||
for (var pub of followPubs) {
|
|
||||||
pubs[JSON.stringify(pub)] = pub;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Object.values(pubs);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAbout(db, id) {
|
|
||||||
var o = await db.get(id + ":about");
|
|
||||||
const k_version = 4;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" content "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" sequence > ?1 AND "+
|
|
||||||
" author = ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'about' AND "+
|
|
||||||
" json_extract(content, '$.about') = author "+
|
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?2 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[f.sequence, id],
|
|
||||||
async function(row) {
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
if (row.content) {
|
|
||||||
var about = {};
|
|
||||||
try {
|
|
||||||
about = JSON.parse(row.content);
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
delete about.about;
|
|
||||||
delete about.type;
|
|
||||||
f.about = Object.assign(f.about, about);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":about", j);
|
|
||||||
}
|
|
||||||
return f.about;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fnv32a(value)
|
|
||||||
{
|
|
||||||
var result = 0x811c9dc5;
|
|
||||||
for (var i = 0; i < value.length; i++) {
|
|
||||||
result ^= value.charCodeAt(i);
|
|
||||||
result += (result << 1) + (result << 4) + (result << 7) + (result << 8) + (result << 24);
|
|
||||||
}
|
|
||||||
return result >>> 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecentPostIds(db, id, ids, limit) {
|
|
||||||
const k_version = 6;
|
|
||||||
var o = await db.get(id + ':recent_posts');
|
|
||||||
var recent = [];
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
var ids_hash = fnv32a(JSON.stringify(ids));
|
|
||||||
if (!f || f.version != k_version || f.ids_hash != ids_hash) {
|
|
||||||
f = {recent: [], rowid: 0, version: k_version, ids_hash: ids_hash};
|
|
||||||
}
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" rowid, "+
|
|
||||||
" id "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" rowid > ? AND "+
|
|
||||||
" author IN (" + ids.map(x => '?').join(", ") + ") AND "+
|
|
||||||
" json_extract(content, '$.type') = 'post' "+
|
|
||||||
"UNION SELECT MAX(rowid) as rowid, NULL FROM messages "+
|
|
||||||
"ORDER BY rowid DESC LIMIT ?",
|
|
||||||
[].concat([f.rowid], ids, [limit + 1]),
|
|
||||||
function(row) {
|
|
||||||
if (row.id) {
|
|
||||||
recent.push(row.id);
|
|
||||||
}
|
|
||||||
if (row.rowid) {
|
|
||||||
f.rowid = row.rowid;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
f.recent = [].concat(recent, f.recent).slice(0, limit);
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":recent_posts", j);
|
|
||||||
}
|
|
||||||
return f.recent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getVotes(db, id) {
|
|
||||||
var o = await db.get(id + ":votes");
|
|
||||||
const k_version = 2;
|
|
||||||
var votes = [];
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {votes: [], rowid: 0, version: k_version};
|
|
||||||
}
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT "+
|
|
||||||
" rowid, "+
|
|
||||||
" author, "+
|
|
||||||
" id, "+
|
|
||||||
" sequence, "+
|
|
||||||
" timestamp, "+
|
|
||||||
" content "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" rowid > ? AND "+
|
|
||||||
" author = ? AND "+
|
|
||||||
" json_extract(content, '$.type') = 'vote' "+
|
|
||||||
"UNION SELECT MAX(rowid) as rowid, NULL, NULL AS id, NULL, NULL, NULL FROM messages "+
|
|
||||||
"ORDER BY rowid DESC LIMIT ?",
|
|
||||||
[f.rowid, id, k_votes_max],
|
|
||||||
async function(row) {
|
|
||||||
if (row.id) {
|
|
||||||
votes.push(row);
|
|
||||||
} else {
|
|
||||||
f.rowid = row.rowid;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
f.votes = [].concat(votes.reverse(), f.votes).slice(0, k_votes_max);
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":votes", j);
|
|
||||||
}
|
|
||||||
return f.votes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPosts(db, ids) {
|
|
||||||
var posts = [];
|
|
||||||
if (ids.length) {
|
|
||||||
await ssb.sqlStream(
|
|
||||||
"SELECT rowid, * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ")",
|
|
||||||
ids,
|
|
||||||
async function(row) {
|
|
||||||
try {
|
|
||||||
posts.push(row);
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ready() {
|
|
||||||
return refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
core.register('onBroadcastsChanged', async function() {
|
|
||||||
await app.postMessage({broadcasts: await ssb.getBroadcasts()});
|
|
||||||
});
|
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function() {
|
|
||||||
var connections = await ssb.connections();
|
|
||||||
await app.postMessage({connections: connections});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
await app.postMessage({clear: true});
|
|
||||||
var whoami = await ssb.whoami();
|
|
||||||
var db = await database("ssb");
|
|
||||||
await Promise.all([
|
|
||||||
app.postMessage({whoami: whoami}),
|
|
||||||
app.postMessage({pubs: await visiblePubs(db, whoami)}),
|
|
||||||
app.postMessage({broadcasts: await ssb.getBroadcasts()}),
|
|
||||||
app.postMessage({connections: await ssb.connections()}),
|
|
||||||
followingDeep(db, [whoami], 2).then(function(f) {
|
|
||||||
getRecentPostIds(db, whoami, [].concat([whoami], f), k_posts_max).then(async function(ids) {
|
|
||||||
return getPosts(db, ids);
|
|
||||||
}).then(async function(posts) {
|
|
||||||
var roots = posts.map(function(x) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(x.content).root;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
roots = roots.filter(function(root) {
|
|
||||||
return root && posts.every(post => post.id != root);
|
|
||||||
});
|
|
||||||
return [].concat(posts, await getPosts(db, roots));
|
|
||||||
}).then(async function(posts) {
|
|
||||||
posts.forEach(async function(post) {
|
|
||||||
await app.postMessage({message: post});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
f.forEach(async function(id) {
|
|
||||||
await Promise.all([
|
|
||||||
getVotes(db, id).then(async function(votes) {
|
|
||||||
return Promise.all(votes.map(vote => app.postMessage({vote: vote})));
|
|
||||||
}),
|
|
||||||
getAbout(db, id).then(async function(user) {
|
|
||||||
return app.postMessage({user: {user: id, about: user}});
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
sendUser(db, whoami),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
core.register('message', async function(m) {
|
|
||||||
if (m.message == 'ready') {
|
|
||||||
await ready();
|
|
||||||
} else if (m.message) {
|
|
||||||
if (m.message.connect) {
|
|
||||||
await ssb.connect(m.message.connect);
|
|
||||||
} else if (m.message.post) {
|
|
||||||
await ssb.post(m.message.post);
|
|
||||||
} else if (m.message.appendMessage) {
|
|
||||||
await ssb.appendMessage(m.message.appendMessage);
|
|
||||||
} else if (m.message.user) {
|
|
||||||
await sendUser(await database("ssb"), m.message.user);
|
|
||||||
} else if (m.message.refresh) {
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print(JSON.stringify(m));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
if (core.user &&
|
|
||||||
core.user.credentials &&
|
|
||||||
core.user.credentials.permissions &&
|
|
||||||
core.user.credentials.permissions.administration) {
|
|
||||||
await app.setDocument(utf8Decode(await getFile("index.html")));
|
|
||||||
} else {
|
|
||||||
await app.setDocument('<div style="color: #f00">Only the administrator can use this app at this time. Login at the top right.</div>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
@ -1,281 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta content="width=device-width,initial-scale=1,minimal-ui" name="viewport">
|
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default-dark.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
|
||||||
<script src="vue-material.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.29.1/commonmark.min.js"></script>
|
|
||||||
<script>
|
|
||||||
var g_data = {
|
|
||||||
whoami: null,
|
|
||||||
connections: [],
|
|
||||||
messages: [],
|
|
||||||
users: {},
|
|
||||||
broadcasts: [],
|
|
||||||
showUsers: false,
|
|
||||||
show_connect_dialog: false,
|
|
||||||
show_user_dialog: null,
|
|
||||||
connect: null,
|
|
||||||
pubs: [],
|
|
||||||
votes: [],
|
|
||||||
};
|
|
||||||
var g_data_initial = JSON.parse(JSON.stringify(g_data));
|
|
||||||
window.addEventListener('message', function(event) {
|
|
||||||
var key = Object.keys(event.data)[0];
|
|
||||||
if (key + 's' in g_data && Array.isArray(g_data[key + 's'])) {
|
|
||||||
g_data[key + 's'].push(event.data[key]);
|
|
||||||
} else if (key == 'user') {
|
|
||||||
Vue.set(g_data.users, event.data.user.user, Object.assign({}, g_data.users[event.data.user.user] || {}, event.data.user.about));
|
|
||||||
} else if (key == 'followers') {
|
|
||||||
if (!g_data.users[event.data.followers.id]) {
|
|
||||||
Vue.set(g_data.users, event.data.followers.id, {});
|
|
||||||
}
|
|
||||||
Vue.set(g_data.users[event.data.followers.id], 'followers', event.data.followers.users);
|
|
||||||
} else if (key == 'following') {
|
|
||||||
if (!g_data.users[event.data.following.id]) {
|
|
||||||
Vue.set(g_data.users, event.data.following.id, {});
|
|
||||||
}
|
|
||||||
Vue.set(g_data.users[event.data.following.id], 'following', event.data.following.users);
|
|
||||||
} else if (key == 'broadcasts') {
|
|
||||||
g_data.broadcasts = event.data.broadcasts;
|
|
||||||
} else if (key == 'pubs') {
|
|
||||||
g_data.pubs = event.data.pubs;
|
|
||||||
} else if (key == 'clear') {
|
|
||||||
Object.keys(g_data_initial).forEach(function(key) {
|
|
||||||
Vue.set(g_data, key, JSON.parse(JSON.stringify(g_data_initial[key])));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
g_data[key] = event.data[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
Vue.use(VueMaterial.default);
|
|
||||||
Vue.component('tf-user', {
|
|
||||||
data: function() { return {users: g_data.users, show_user_dialog: false, show_follow_dialog: false} },
|
|
||||||
props: ['id'],
|
|
||||||
mounted: function() {
|
|
||||||
window.parent.postMessage({user: this.id}, '*');
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
following: {
|
|
||||||
get: function() {
|
|
||||||
return g_data.users[g_data.whoami] &&
|
|
||||||
g_data.users[g_data.whoami].following &&
|
|
||||||
g_data.users[g_data.whoami].following.indexOf(this.id) != -1;
|
|
||||||
},
|
|
||||||
set: function(newValue) {
|
|
||||||
if (g_data.users[g_data.whoami] &&
|
|
||||||
g_data.users[g_data.whoami].following) {
|
|
||||||
if (newValue && g_data.users[g_data.whoami].following.indexOf(this.id) == -1) {
|
|
||||||
window.parent.postMessage({appendMessage: {type: "contact", following: true, contact: this.id}}, '*');
|
|
||||||
} else if (!newValue) {
|
|
||||||
window.parent.postMessage({appendMessage: {type: "contact", following: false, contact: this.id}}, '*');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
template: `<span @click="show_user_dialog = true">
|
|
||||||
{{users[id] && users[id].name ? users[id].name : id}}
|
|
||||||
<md-tooltip v-if="users[id] && users[id].name">{{id}}</md-tooltip>
|
|
||||||
<md-dialog :md-active.sync="show_user_dialog">
|
|
||||||
<md-dialog-title>{{users[id] && users[id].name ? users[id].name : id}}</md-dialog-title>
|
|
||||||
<md-dialog-content v-if="users[id]">
|
|
||||||
<div v-if="users[id].image"><img :src="'/' + users[id].image + '/view'"></div>
|
|
||||||
<div v-if="users[id].name">{{id}}</div>
|
|
||||||
<div>{{users[id].description}}</div>
|
|
||||||
<div><md-switch v-model="following">Following</md-switch></div>
|
|
||||||
<md-list>
|
|
||||||
<md-subheader>Followers</md-subheader>
|
|
||||||
<md-list-item v-for="follower in (users[id] || []).followers" v-bind:key="'follower-' + follower">
|
|
||||||
<tf-user :id="follower"></tf-user>
|
|
||||||
</md-list-item>
|
|
||||||
<md-subheader>Following</md-subheader>
|
|
||||||
<md-list-item v-for="user in (users[id] || []).following" v-bind:key="'following-' + user">
|
|
||||||
<tf-user :id="user"></tf-user>
|
|
||||||
</md-list-item>
|
|
||||||
</md-list>
|
|
||||||
</md-dialog-content>
|
|
||||||
<md-dialog-actions>
|
|
||||||
<md-button @click="show_user_dialog = false">Close</md-button>
|
|
||||||
</md-dialog-actions>
|
|
||||||
</md-dialog>
|
|
||||||
</span>`,
|
|
||||||
});
|
|
||||||
Vue.component('tf-message', {
|
|
||||||
props: ['message', 'messages'],
|
|
||||||
data: function() { return { showRaw: false } },
|
|
||||||
computed: {
|
|
||||||
content_json: function() {
|
|
||||||
try {
|
|
||||||
return JSON.parse(this.message.content);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sub_messages: function() {
|
|
||||||
var id = this.message.id;
|
|
||||||
return this.messages.filter(function (x) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(x.content).root == id;
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
votes: function() {
|
|
||||||
return [];
|
|
||||||
var id = this.message.id;
|
|
||||||
return this.votes.filter(function (x) {
|
|
||||||
try {
|
|
||||||
var j = JSON.parse(x.content);
|
|
||||||
return j.type == 'vote' && j.vote.link == id;
|
|
||||||
} catch {}
|
|
||||||
}).reduce(function (accum, value) {
|
|
||||||
var expression = JSON.parse(value.content).vote.expression;
|
|
||||||
if (!accum[expression]) {
|
|
||||||
accum[expression] = [];
|
|
||||||
}
|
|
||||||
accum[expression].push(value);
|
|
||||||
return accum;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
markdown: function(md) {
|
|
||||||
var reader = new commonmark.Parser({safe: true});
|
|
||||||
var writer = new commonmark.HtmlRenderer();
|
|
||||||
return writer.render(reader.parse(md));
|
|
||||||
},
|
|
||||||
json: function(message) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(message.content);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
template: `<md-app class="md-elevation-8" style="margin: 1em" v-if="!content_json || ['pub', 'vote'].indexOf(content_json.type) == -1">
|
|
||||||
<md-app-toolbar>
|
|
||||||
<h3>
|
|
||||||
<tf-user :id="message.author"></tf-user>
|
|
||||||
</h3>
|
|
||||||
<div style="font-size: x-small">{{new Date(message.timestamp)}}</div>
|
|
||||||
<div class="md-toolbar-section-end">
|
|
||||||
<md-menu>
|
|
||||||
<md-button md-menu-trigger class="md-icon-button"><md-icon>more_vert</md-icon></md-button>
|
|
||||||
<md-menu-content>
|
|
||||||
<md-menu-item v-if="!showRaw" v-on:click="showRaw = true">View Raw</md-menu-item>
|
|
||||||
<md-menu-item v-else v-on:click="showRaw = false">View Message</md-menu-item>
|
|
||||||
</md-menu-content>
|
|
||||||
</md-menu>
|
|
||||||
</div>
|
|
||||||
</md-app-toolbar>
|
|
||||||
<md-app-content>
|
|
||||||
<div v-if="showRaw">{{message.content}}</div>
|
|
||||||
<div v-else>
|
|
||||||
<div v-if="content_json && content_json.type == 'post'">
|
|
||||||
<div v-html="this.markdown(content_json.text)"></div>
|
|
||||||
<img v-for="mention in content_json.mentions" v-if="mention.link && typeof(mention.link) == 'string' && mention.link.startsWith('&')" :src="'/' + mention.link + '/view'"></img>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="content_json && content_json.type == 'contact'"><tf-user :id="message.author"></tf-user> {{content_json.following ? '==>' : '=/=>'}} <tf-user :id="content_json.contact"></tf-user></div>
|
|
||||||
<div v-else>{{message.content}}</div>
|
|
||||||
</div>
|
|
||||||
<tf-message v-for="sub_message in sub_messages" v-bind:message="sub_message" v-bind:messages="messages" v-bind:key="sub_message.id"></tf-message>
|
|
||||||
<md-chip v-for="vote in Object.keys(votes)" v-bind:key="vote">
|
|
||||||
{{vote + (votes[vote].length > 1 ? ' (' + votes[vote].length + ')' : '')}}
|
|
||||||
</md-chip>
|
|
||||||
</md-app-content>
|
|
||||||
</md-app>`,
|
|
||||||
});
|
|
||||||
function markdown(d) { return d; }
|
|
||||||
Vue.config.performance = true;
|
|
||||||
var vue = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: g_data,
|
|
||||||
methods: {
|
|
||||||
post_message: function() {
|
|
||||||
window.parent.postMessage({post: document.getElementById('post_text').value}, '*');
|
|
||||||
},
|
|
||||||
ssb_connect: function(connection) {
|
|
||||||
window.parent.postMessage({connect: connection}, '*');
|
|
||||||
},
|
|
||||||
content_json: function(message) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(message.content);
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
refresh: function() {
|
|
||||||
window.parent.postMessage({refresh: true}, '*');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.parent.postMessage('ready', '*');
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body style="color: #fff">
|
|
||||||
<div id="app">
|
|
||||||
<md-dialog :md-active.sync="show_connect_dialog">
|
|
||||||
<md-dialog-title>Connect</md-dialog-title>
|
|
||||||
<md-dialog-content>
|
|
||||||
<md-field>
|
|
||||||
<label>net:127.0.0.1:8008~shs:id</label>
|
|
||||||
<md-input v-model="connect"></md-input>
|
|
||||||
</md-field>
|
|
||||||
</md-dialog-content>
|
|
||||||
<md-dialog-actions>
|
|
||||||
<md-button class="md-primary" @click="ssb_connect(connect); connect = null; show_connect_dialog = false">Connect</md-button>
|
|
||||||
<md-button @click="connect = null; show_connect_dialog = false">Cancel</md-button>
|
|
||||||
</md-dialog-actions>
|
|
||||||
</md-dialog>
|
|
||||||
<md-app style="position: absolute; height: 100%; width: 100%">
|
|
||||||
<md-app-toolbar class="md-primary">
|
|
||||||
<md-button class="md-icon-button" @click="showUsers = !showUsers">
|
|
||||||
<md-icon>menu</md-icon>
|
|
||||||
</md-button>
|
|
||||||
<span class="md-title">Tilde Friends Secure Scuttlebutt Test</span>
|
|
||||||
</md-app-toolbar>
|
|
||||||
<md-app-drawer :md-active.sync="showUsers" md-persistent="full">
|
|
||||||
<md-list>
|
|
||||||
<md-subheader>Followers</md-subheader>
|
|
||||||
<md-list-item v-for="follower in (users[whoami] || []).followers" v-bind:key="'follower-' + follower"><tf-user :id="follower"></tf-user></md-list-item>
|
|
||||||
<md-subheader>Following</md-subheader>
|
|
||||||
<md-list-item v-for="user in (users[whoami] || []).following" v-bind:key="'following-' + user"><tf-user :id="user"></tf-user></md-list-item>
|
|
||||||
<md-subheader>Network</md-subheader>
|
|
||||||
<md-list-item v-for="broadcast in broadcasts" v-bind:key="JSON.stringify(broadcast)" @click="ssb_connect(broadcast)">{{broadcast.address}}:{{broadcast.port}} <tf-user :id="broadcast.pubkey"></tf-user></md-list-item>
|
|
||||||
<md-subheader>Pubs</md-subheader>
|
|
||||||
<md-list-item v-for="pub in pubs" v-bind:key="JSON.stringify(pub)" @click="ssb_connect({address: pub.host, port: pub.port, pubkey: pub.key})">{{pub.host}}:{{pub.port}} <tf-user :id="pub.key"></tf-user></md-list-item>
|
|
||||||
<md-subheader>Connections</md-subheader>
|
|
||||||
<md-list-item v-for="connection in connections" v-bind:key="'connection-' + JSON.stringify(connection)"><tf-user :id="connection"></tf-user></md-list-item>
|
|
||||||
<md-list-item @click="show_connect_dialog = true">Connect</md-list-item>
|
|
||||||
</md-list>
|
|
||||||
</md-app-drawer>
|
|
||||||
<md-app-content>
|
|
||||||
<md-button @click="refresh()" class="md-icon-button md-dense md-raised md-primary">
|
|
||||||
<md-icon>cached</md-icon>
|
|
||||||
</md-button>
|
|
||||||
Welcome, <tf-user :id="whoami"></tf-user>.
|
|
||||||
<md-card class="md-elevation-8">
|
|
||||||
<md-card-header>
|
|
||||||
<div class="md-title">What's up?</div>
|
|
||||||
</md-card-header>
|
|
||||||
<md-card-content>
|
|
||||||
<md-field>
|
|
||||||
<label>Post a message</label>
|
|
||||||
<md-textarea id="post_text"></md-textarea>
|
|
||||||
</md-field>
|
|
||||||
</md-card-content>
|
|
||||||
<md-card-actions>
|
|
||||||
<md-button class="md-raised md-primary" v-on:click="post_message()">Submit Post</md-button>
|
|
||||||
</md-card-actions>
|
|
||||||
</md-card>
|
|
||||||
<tf-message v-for="message in messages" v-if="!content_json(message).root" v-bind:message="message" v-bind:messages="messages" v-bind:key="message.id"></tf-message>
|
|
||||||
</md-app-content>
|
|
||||||
</md-app>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
File diff suppressed because one or more lines are too long
1
apps/cory/ssb.json
Normal file
1
apps/cory/ssb.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&gxOJaVf/HdjVJVC9NvZ9n3/825OD1xMMHdF/dFQwe24=.sha256","lit-all.min.js":"&XKgdRySJuiZeZvchNFGjVWn0XOVhQFmG7/HTWYQ8s68=.sha256","index.html":"&TxhFekB9ov7tf/fmkAg7x5797i27oLidhgxEfDKC0T0=.sha256","script.js":"&G8puK9Q4MngHy3D4ppcKyT49WKbHD2OCeUcAw2ghTDE=.sha256","lit-all.min.js.map":"&lA9iFp1YbqSndxXZuwtgmrj7NDMkN71nJITbtjWL3VA=.sha256","tf-id-picker.js":"&maN8DUFrmRxW5nsVyOAMk5k1ekcz/pfzvSS99ac3jo8=.sha256","tf-app.js":"&7hclNu41CIoNk1JlXHiYmDPDyDIICZfMickJYtnF5eQ=.sha256","tf-message.js":"&oXFucwmn16nvKslQoGKTppO+71EoDZJE54z3WrlNUPI=.sha256","tf-user.js":"&bXTedgBudTQLXEBPY9R8OLfQ/ZLpo8YRU9Oq/wuGG3Y=.sha256","tf-utils.js":"&6RQUuxB3PkOhYEJr9+89Ptx7uijczjn0r035yCcQOQQ=.sha256","commonmark.min.js":"&bfBaMLU19d1p/vPBF9hlARqDX002KXG/UOfxOahZhe4=.sha256","tf-compose.js":"&y+Q47tdm60Od1UzuRu7OOLwineyQCL1LIb3KP5IwHTY=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&pqYLDE/13PyEt2ceeFqvnwZ8NqWfPfpDBt4vP8SeHbs=.sha256","tf-styles.js":"&LFeL/vWgrv4N8q/mBrQAnhbaOI+dXNJYvH9bn1bXSqQ=.sha256","tf-profile.js":"&vRKjsnYvOiHCQahzEfznCvP5YDwUPtltlpWf+pxwZ1Y=.sha256","commonmark-linkify.js":"&X+hNNkmSRvKY86khyAun+cXksquXbMakZdINbGbx30g=.sha256","tf-tab-search.js":"&ESt2vMG19sH5j6ungKua/ZuvIGslyuWyb3juXdOCecg=.sha256","tf-tab-news.js":"&F7T3LVS867x7vsKhYRR7eLNdCFZmrZ3JzEMfJEEKRm0=.sha256","tf-tab-connections.js":"&Ftt5RnkrhndV2lwC7XXUZX8JiUODqPjqEVgSTJQD6JU=.sha256","tf-news.js":"&gfG5LwXpugDkwDCOCOxQnNn0jLURZexSmvDu4SpQohA=.sha256","tribute.css":"&9FogMzZHKXCfGb7mlh7z+/wiNZzBsOB/tKoh6MfYJno=.sha256","tribute.esm.js":"&P1wKqCfYULpR/ahSB98JP8xaxfikuZwwtT6I/SAo7/Y=.sha256","commonmark-hashtag.js":"&H+V1OLA9GDdzycKclz276zAtSZLpT3rlNVa4+qQmp4o=.sha256"}}
|
97
apps/cory/ssb/app.js
Normal file
97
apps/cory/ssb/app.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_database;
|
||||||
|
let g_hash;
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseGet(key) {
|
||||||
|
return g_database ? g_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseSet(key, value) {
|
||||||
|
return g_database ? g_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createIdentity() {
|
||||||
|
return ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getAllIdentities() {
|
||||||
|
return ssb.getAllIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getBroadcasts() {
|
||||||
|
return ssb.getBroadcasts();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getConnections() {
|
||||||
|
return ssb.connections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function connectionSendJson(id, message) {
|
||||||
|
return ssb.connectionSendJson(id, message);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createTunnel(portal, request_number, target) {
|
||||||
|
let t = ssb.createTunnel(portal, request_number, target);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function connect(token) {
|
||||||
|
await ssb.connect(token);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function closeConnection(id) {
|
||||||
|
await ssb.closeConnection(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlStream(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hashChanged(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tfrpc.register(function getHash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
tfrpc.register(function setHash(hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('message', async function(id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(function apps() {
|
||||||
|
return core.apps();
|
||||||
|
});
|
||||||
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('onConnectionsChanged', async function() {
|
||||||
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (typeof(database) !== 'undefined') {
|
||||||
|
g_database = await database('ssb');
|
||||||
|
}
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
main();
|
90
apps/cory/ssb/commonmark-hashtag.js
Normal file
90
apps/cory/ssb/commonmark-hashtag.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
function textNode(text) {
|
||||||
|
const node = new commonmark.Node("text", undefined);
|
||||||
|
node.literal = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkNode(text, link) {
|
||||||
|
const linkNode = new commonmark.Node("link", undefined);
|
||||||
|
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||||
|
linkNode.appendChild(textNode(text));
|
||||||
|
return linkNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMatches(text, regexp) {
|
||||||
|
// Regexp must be sticky.
|
||||||
|
regexp = new RegExp(regexp, "gm");
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let match = regexp.exec(text);
|
||||||
|
while (match) {
|
||||||
|
const matchText = match[0];
|
||||||
|
|
||||||
|
if (match.index > i) {
|
||||||
|
result.push([text.substring(i, match.index), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([matchText, true]);
|
||||||
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
|
match = regexp.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < text.length) {
|
||||||
|
result.push([text.substring(i, text.length), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp("#[\\w-]+");
|
||||||
|
|
||||||
|
function split(textNodes) {
|
||||||
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
const parts = splitMatches(text, regex);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part[1]) {
|
||||||
|
return linkNode(part[0], part[0]);
|
||||||
|
} else {
|
||||||
|
return textNode(part[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transform(parsed) {
|
||||||
|
const walker = parsed.walker();
|
||||||
|
let event;
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
const node = event.node;
|
||||||
|
if (event.entering && node.type === "text") {
|
||||||
|
nodes.push(node);
|
||||||
|
} else {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
split(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
split(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
91
apps/cory/ssb/commonmark-linkify.js
Normal file
91
apps/cory/ssb/commonmark-linkify.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
function textNode(text) {
|
||||||
|
const node = new commonmark.Node("text", undefined);
|
||||||
|
node.literal = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkNode(text, url) {
|
||||||
|
const urlNode = new commonmark.Node("link", undefined);
|
||||||
|
urlNode.destination = url;
|
||||||
|
urlNode.appendChild(textNode(text));
|
||||||
|
|
||||||
|
return urlNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMatches(text, regexp) {
|
||||||
|
// Regexp must be sticky.
|
||||||
|
regexp = new RegExp(regexp, "gm");
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let match = regexp.exec(text);
|
||||||
|
while (match) {
|
||||||
|
const matchText = match[0];
|
||||||
|
|
||||||
|
if (match.index > i) {
|
||||||
|
result.push([text.substring(i, match.index), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([matchText, true]);
|
||||||
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
|
match = regexp.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < text.length) {
|
||||||
|
result.push([text.substring(i, text.length), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||||
|
|
||||||
|
function splitURLs(textNodes) {
|
||||||
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
const parts = splitMatches(text, urlRegexp);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part[1]) {
|
||||||
|
return linkNode(part[0], part[0]);
|
||||||
|
} else {
|
||||||
|
return textNode(part[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transform(parsed) {
|
||||||
|
const walker = parsed.walker();
|
||||||
|
let event;
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
const node = event.node;
|
||||||
|
if (event.entering && node.type === "text") {
|
||||||
|
nodes.push(node);
|
||||||
|
} else {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
1
apps/cory/ssb/commonmark.min.js
vendored
Normal file
1
apps/cory/ssb/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
85
apps/cory/ssb/emojis.js
Normal file
85
apps/cory/ssb/emojis.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
let g_emojis;
|
||||||
|
|
||||||
|
function get_emojis() {
|
||||||
|
if (g_emojis) {
|
||||||
|
return Promise.resolve(g_emojis);
|
||||||
|
}
|
||||||
|
return fetch('emojis.json').then(function(result) {
|
||||||
|
g_emojis = result.json();
|
||||||
|
return g_emojis;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function picker(callback, anchor) {
|
||||||
|
get_emojis().then(function(json) {
|
||||||
|
let existing = document.getElementById('emoji_picker');
|
||||||
|
if (existing) {
|
||||||
|
existing.parentElement.removeChild(existing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.id = 'emoji_picker';
|
||||||
|
div.style.color = '#000';
|
||||||
|
div.style.background = '#fff';
|
||||||
|
div.style.border = '1px solid #000';
|
||||||
|
div.style.display = 'block';
|
||||||
|
div.style.position = 'absolute';
|
||||||
|
div.style.maxWidth = '16em';
|
||||||
|
div.style.maxHeight = '16em';
|
||||||
|
div.style.overflow = 'scroll';
|
||||||
|
div.style.fontWeight = 'bold';
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
div.appendChild(input);
|
||||||
|
let list = document.createElement('div');
|
||||||
|
div.appendChild(list);
|
||||||
|
function refresh() {
|
||||||
|
while (list.firstChild) {
|
||||||
|
list.removeChild(list.firstChild);
|
||||||
|
}
|
||||||
|
let search = input.value;
|
||||||
|
Object.entries(json).forEach(function(row) {
|
||||||
|
let header = document.createElement('div');
|
||||||
|
header.appendChild(document.createTextNode(row[0]));
|
||||||
|
list.appendChild(header);
|
||||||
|
let any = false;
|
||||||
|
for (let entry of row[1]) {
|
||||||
|
if (search &&
|
||||||
|
search.length &&
|
||||||
|
entry.name.indexOf(search) == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emoji = document.createElement('span');
|
||||||
|
const k_size = '1.25em';
|
||||||
|
emoji.style.width = k_size;
|
||||||
|
emoji.style.maxWidth = k_size;
|
||||||
|
emoji.style.minWidth = k_size;
|
||||||
|
emoji.style.height = k_size;
|
||||||
|
emoji.style.maxHeight = k_size;
|
||||||
|
emoji.style.minHeight = k_size;
|
||||||
|
emoji.style.display = 'inline-block';
|
||||||
|
emoji.style.overflow = 'hidden';
|
||||||
|
emoji.style.cursor = 'pointer';
|
||||||
|
emoji.onclick = function() {
|
||||||
|
callback(entry);
|
||||||
|
div.parentElement.removeChild(div);
|
||||||
|
}
|
||||||
|
emoji.title = entry.name;
|
||||||
|
emoji.appendChild(document.createTextNode(entry.emoji));
|
||||||
|
list.appendChild(emoji);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
list.removeChild(header);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
input.oninput = refresh;
|
||||||
|
document.body.appendChild(div);
|
||||||
|
div.style.position = 'fixed';
|
||||||
|
div.style.top = '50%';
|
||||||
|
div.style.left = '50%';
|
||||||
|
div.style.transform = 'translate(-50%, -50%)';
|
||||||
|
});
|
||||||
|
}
|
15115
apps/cory/ssb/emojis.json
Normal file
15115
apps/cory/ssb/emojis.json
Normal file
File diff suppressed because it is too large
Load Diff
21
apps/cory/ssb/index.html
Normal file
21
apps/cory/ssb/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html style="color: #fff">
|
||||||
|
<head>
|
||||||
|
<title>Tilde Friends</title>
|
||||||
|
<base target="_top">
|
||||||
|
<link rel="stylesheet" href="tribute.css" />
|
||||||
|
<style>
|
||||||
|
.tribute-container {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<tf-app/>
|
||||||
|
<script>window.litDisableBundleWarning = true;</script>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
|
<script src="commonmark-hashtag.js" type="module"></script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
132
apps/cory/ssb/lit-all.min.js
vendored
Normal file
132
apps/cory/ssb/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/cory/ssb/lit-all.min.js.map
Normal file
1
apps/cory/ssb/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
13
apps/cory/ssb/script.js
Normal file
13
apps/cory/ssb/script.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
import * as tf_id_picker from './tf-id-picker.js';
|
||||||
|
import * as tf_app from './tf-app.js';
|
||||||
|
import * as tf_message from './tf-message.js';
|
||||||
|
import * as tf_user from './tf-user.js';
|
||||||
|
import * as tf_compose from './tf-compose.js';
|
||||||
|
import * as tf_news from './tf-news.js';
|
||||||
|
import * as tf_profile from './tf-profile.js';
|
||||||
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
|
import * as tf_tab_search from './tf-tab-search.js';
|
||||||
|
import * as tf_tab_connections from './tf-tab-connections.js';
|
330
apps/cory/ssb/tf-app.js
Normal file
330
apps/cory/ssb/tf-app.js
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
hash: {type: String},
|
||||||
|
unread: {type: Array},
|
||||||
|
tab: {type: String},
|
||||||
|
broadcasts: {type: Array},
|
||||||
|
connections: {type: Array},
|
||||||
|
loading: {type: Boolean},
|
||||||
|
loaded: {type: Boolean},
|
||||||
|
following: {type: Array},
|
||||||
|
users: {type: Object},
|
||||||
|
ids: {type: Array},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.hash = '#';
|
||||||
|
this.unread = [];
|
||||||
|
this.tab = 'news';
|
||||||
|
this.broadcasts = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.following = [];
|
||||||
|
this.users = {};
|
||||||
|
this.loaded = false;
|
||||||
|
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
||||||
|
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
||||||
|
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
||||||
|
tfrpc.register(function hashChanged(hash) {
|
||||||
|
self.set_hash(hash);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function notifyNewMessage(id) {
|
||||||
|
await self.fetch_new_message(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(function set(name, value) {
|
||||||
|
if (name === 'broadcasts') {
|
||||||
|
self.broadcasts = value;
|
||||||
|
} else if (name === 'connections') {
|
||||||
|
self.connections = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.initial_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initial_load() {
|
||||||
|
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
||||||
|
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_hash(hash) {
|
||||||
|
this.hash = hash || '#';
|
||||||
|
if (this.hash.startsWith('#q=')) {
|
||||||
|
this.tab = 'search';
|
||||||
|
} else if (this.hash === '#connections') {
|
||||||
|
this.tab = 'connections';
|
||||||
|
} else {
|
||||||
|
this.tab = 'news';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async contacts_internal(id, last_row_id, following, max_row_id) {
|
||||||
|
let result = Object.assign({}, following[id] || {});
|
||||||
|
result.following = result.following || {};
|
||||||
|
result.blocking = result.blocking || {};
|
||||||
|
let contacts = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT content FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
rowid > ? AND
|
||||||
|
rowid <= ? AND
|
||||||
|
json_extract(content, "$.type") = "contact"
|
||||||
|
ORDER BY sequence
|
||||||
|
`,
|
||||||
|
[id, last_row_id, max_row_id]);
|
||||||
|
for (let row of contacts) {
|
||||||
|
let contact = JSON.parse(row.content);
|
||||||
|
if (contact.following === true) {
|
||||||
|
result.following[contact.contact] = true;
|
||||||
|
} else if (contact.following === false) {
|
||||||
|
delete result.following[contact.contact];
|
||||||
|
} else if (contact.blocking === true) {
|
||||||
|
result.blocking[contact.contact] = true;
|
||||||
|
} else if (contact.blocking === false) {
|
||||||
|
delete result.blocking[contact.contact];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
following[id] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async contact(id, last_row_id, following, max_row_id) {
|
||||||
|
return await this.contacts_internal(id, last_row_id, following, max_row_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
||||||
|
let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
|
||||||
|
let result = {};
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
let id = ids[i];
|
||||||
|
let contact = contacts[i];
|
||||||
|
let found = Object.keys(contact.following).filter(y => !contact.blocking[y]);
|
||||||
|
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, Object.assign({}, contact.blocking, blocking), last_row_id, following, max_row_id) : [];
|
||||||
|
result[id] = [id, ...found, ...deeper];
|
||||||
|
}
|
||||||
|
return [...new Set(Object.values(result).flat())];
|
||||||
|
}
|
||||||
|
|
||||||
|
async following_deep(ids, depth, blocking) {
|
||||||
|
const k_cache_version = 5;
|
||||||
|
let cache = await tfrpc.rpc.databaseGet('following');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
following: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = (await tfrpc.rpc.query(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`, []))[0].max_row_id;
|
||||||
|
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
await tfrpc.rpc.databaseSet('following', JSON.stringify(cache));
|
||||||
|
return [result, cache.following];
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_about(ids, users) {
|
||||||
|
const k_cache_version = 1;
|
||||||
|
let cache = await tfrpc.rpc.databaseGet('about');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
about: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = (await tfrpc.rpc.query(`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`, []))[0].max_row_id;
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
if (ids.indexOf(id) == -1) {
|
||||||
|
delete cache.about[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let abouts = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?1) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid > ?3 AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?2) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
ORDER BY messages.author, messages.sequence
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(ids.filter(id => cache.about[id])),
|
||||||
|
JSON.stringify(ids.filter(id => !cache.about[id])),
|
||||||
|
cache.last_row_id,
|
||||||
|
max_row_id,
|
||||||
|
]);
|
||||||
|
for (let about of abouts) {
|
||||||
|
let content = JSON.parse(about.content);
|
||||||
|
if (content.about === about.author) {
|
||||||
|
delete content.type;
|
||||||
|
delete content.about;
|
||||||
|
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
|
||||||
|
users = users || {};
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
||||||
|
}
|
||||||
|
return Object.assign({}, users);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_new_message(id) {
|
||||||
|
let messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.id = ?
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
id,
|
||||||
|
]);
|
||||||
|
if (messages && messages.length) {
|
||||||
|
this.unread = [...this.unread, ...messages];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _handle_whoami_changed(event) {
|
||||||
|
let old_id = this.whoami;
|
||||||
|
let new_id = event.srcElement.selected;
|
||||||
|
console.log('received', new_id);
|
||||||
|
if (this.whoami !== new_id) {
|
||||||
|
console.log(event);
|
||||||
|
this.whoami = new_id;
|
||||||
|
console.log(`whoami ${old_id} => ${new_id}`);
|
||||||
|
await tfrpc.rpc.localStorageSet('whoami', new_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_identity() {
|
||||||
|
if (confirm("Are you sure you want to create a new identity?")) {
|
||||||
|
await tfrpc.rpc.createIdentity();
|
||||||
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_id_picker() {
|
||||||
|
return html`
|
||||||
|
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
||||||
|
<button @click=${this.create_identity}>Create Identity</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let whoami = this.whoami;
|
||||||
|
let [following, users] = await this.following_deep([whoami], 2, {});
|
||||||
|
users = await this.fetch_about(following.sort(), users);
|
||||||
|
this.following = following;
|
||||||
|
this.users = users;
|
||||||
|
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||||
|
this.whoami = whoami;
|
||||||
|
this.loaded = whoami;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_tab() {
|
||||||
|
let following = this.following;
|
||||||
|
let users = this.users;
|
||||||
|
if (this.tab === 'news') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
||||||
|
`;
|
||||||
|
} else if (this.tab === 'connections') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
||||||
|
`;
|
||||||
|
} else if (this.tab === 'search') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_fake_news() {
|
||||||
|
this.unread = [{
|
||||||
|
author: this.whoami,
|
||||||
|
placeholder: true,
|
||||||
|
id: '%fake_id',
|
||||||
|
text: 'text',
|
||||||
|
content: 'hello',
|
||||||
|
}, ...this.unread];
|
||||||
|
}
|
||||||
|
|
||||||
|
async set_tab(tab) {
|
||||||
|
this.tab = tab;
|
||||||
|
if (tab === 'news') {
|
||||||
|
await tfrpc.rpc.setHash('#');
|
||||||
|
} else if (tab === 'connections') {
|
||||||
|
await tfrpc.rpc.setHash('#connections');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
|
console.log(`starting loading ${this.whoami} ${this.loaded}`);
|
||||||
|
this.loading = true;
|
||||||
|
this.load().finally(function() {
|
||||||
|
self.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tabs = html`
|
||||||
|
<div>
|
||||||
|
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
||||||
|
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
||||||
|
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
let contents =
|
||||||
|
!this.loaded ?
|
||||||
|
this.loading ?
|
||||||
|
html`<div>Loading...</div>` :
|
||||||
|
html`<div>Select or create an identity.</div>` :
|
||||||
|
this.render_tab();
|
||||||
|
return html`
|
||||||
|
${this.render_id_picker()}
|
||||||
|
${tabs}
|
||||||
|
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
||||||
|
${contents}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-app', TfElement);
|
265
apps/cory/ssb/tf-compose.js
Normal file
265
apps/cory/ssb/tf-compose.js
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
import Tribute from './tribute.esm.js';
|
||||||
|
|
||||||
|
class TfComposeElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
root: {type: String},
|
||||||
|
branch: {type: String},
|
||||||
|
mentions: {type: Object},
|
||||||
|
apps: {type: Object},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.users = {};
|
||||||
|
this.root = undefined;
|
||||||
|
this.branch = undefined;
|
||||||
|
this.mentions = {};
|
||||||
|
this.apps = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
let text = edit.value;
|
||||||
|
|
||||||
|
/* Update mentions. */
|
||||||
|
for (let match of text.matchAll(/\[([^\[]+)]\(([@&%][^\)]+)/g)) {
|
||||||
|
let name = match[1];
|
||||||
|
let link = match[2];
|
||||||
|
let balance = 0;
|
||||||
|
let bracket_end = match.index + match[1].length + '[]'.length - 1;
|
||||||
|
for (let i = bracket_end; i >= 0; i--) {
|
||||||
|
if (text.charAt(i) == ']') {
|
||||||
|
balance++;
|
||||||
|
} else if (text.charAt(i) == '[') {
|
||||||
|
balance--;
|
||||||
|
}
|
||||||
|
if (balance <= 0) {
|
||||||
|
name = text.substring(i + 1, bracket_end);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.mentions[link]) {
|
||||||
|
this.mentions[link] = {
|
||||||
|
link: link,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||||
|
this.mentions = Object.assign({}, this.mentions);
|
||||||
|
}
|
||||||
|
|
||||||
|
preview.innerHTML = tfutils.markdown(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_to_webp(buffer, type) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
let img = new Image();
|
||||||
|
img.onload = function() {
|
||||||
|
let canvas = document.createElement('canvas');
|
||||||
|
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||||
|
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||||
|
let scale = Math.min(width_scale, height_scale);
|
||||||
|
canvas.width = img.width * scale;
|
||||||
|
canvas.height = img.height * scale;
|
||||||
|
let context = canvas.getContext('2d');
|
||||||
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
let data_url = canvas.toDataURL('image/webp');
|
||||||
|
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
img.onerror = function(event) {
|
||||||
|
reject(new Error('Failed to load image.'));
|
||||||
|
};
|
||||||
|
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
||||||
|
let original = `data:${type};base64,${btoa(raw)}`;
|
||||||
|
img.src = original;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async add_file(file) {
|
||||||
|
try {
|
||||||
|
let self = this;
|
||||||
|
let buffer = await file.arrayBuffer();
|
||||||
|
let type = file.type;
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
buffer = await self.convert_to_webp(buffer, file.type);
|
||||||
|
type = 'image/webp';
|
||||||
|
} else {
|
||||||
|
buffer = Array.from(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
let id = await tfrpc.rpc.store_blob(buffer);
|
||||||
|
let name = type.split('/')[0] + ':' + file.name;
|
||||||
|
self.mentions[id] = {
|
||||||
|
link: id,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
size: buffer.length ?? buffer.byteLength,
|
||||||
|
};
|
||||||
|
self.mentions = Object.assign({}, self.mentions);
|
||||||
|
let edit = self.renderRoot.getElementById('edit');
|
||||||
|
edit.value += `\n`;
|
||||||
|
self.changed();
|
||||||
|
} catch(e) {
|
||||||
|
alert(e?.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paste(event) {
|
||||||
|
let self = this;
|
||||||
|
for (let item of event.clipboardData.items) {
|
||||||
|
if (item.type?.startsWith('image/')) {
|
||||||
|
let file = item.getAsFile();
|
||||||
|
if (!file) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.add_file(file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
let self = this;
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
let message = {
|
||||||
|
type: 'post',
|
||||||
|
text: edit.value,
|
||||||
|
};
|
||||||
|
if (this.root || this.branch) {
|
||||||
|
message.root = this.root;
|
||||||
|
message.branch = this.branch;
|
||||||
|
}
|
||||||
|
if (Object.values(this.mentions).length) {
|
||||||
|
message.mentions = Object.values(this.mentions);
|
||||||
|
}
|
||||||
|
console.log('Would post:', message);
|
||||||
|
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
|
edit.value = '';
|
||||||
|
self.mentions = {};
|
||||||
|
self.changed();
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discard() {
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
edit.value = '';
|
||||||
|
this.changed();
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-discard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
let self = this;
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.onchange = function(event) {
|
||||||
|
let file = event.target.files[0];
|
||||||
|
self.add_file(file);
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
let tribute = new Tribute({
|
||||||
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
|
selectTemplate: function(item) {
|
||||||
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_mention(id) {
|
||||||
|
delete this.mentions[id];
|
||||||
|
this.mentions = Object.assign({}, this.mentions);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_mention(mention) {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
|
||||||
|
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_attach_app() {
|
||||||
|
let self = this;
|
||||||
|
|
||||||
|
async function attach_selected_app() {
|
||||||
|
let name = self.renderRoot.getElementById('select').value;
|
||||||
|
let id = self.apps[name];
|
||||||
|
let mentions = {};
|
||||||
|
mentions[id] = {
|
||||||
|
name: name,
|
||||||
|
link: id,
|
||||||
|
type: 'application/tildefriends',
|
||||||
|
};
|
||||||
|
if (name && id) {
|
||||||
|
let app = JSON.parse(await tfrpc.rpc.get_blob(id));
|
||||||
|
for (let entry of Object.entries(app.files)) {
|
||||||
|
mentions[entry[1]] = {
|
||||||
|
name: entry[0],
|
||||||
|
link: entry[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mentions = Object.assign(this.mentions || {}, mentions);
|
||||||
|
this.apps = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.apps) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<select id="select">
|
||||||
|
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
||||||
|
</select>
|
||||||
|
<input type="button" value="Attach" @click=${attach_selected_app}></input>
|
||||||
|
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_attach_app_button() {
|
||||||
|
async function attach_app() {
|
||||||
|
this.apps = await tfrpc.rpc.apps();
|
||||||
|
}
|
||||||
|
if (!this.apps) {
|
||||||
|
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
|
||||||
|
} else {
|
||||||
|
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
let result = html`
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<textarea id="edit" @input=${this.changed} @paste=${this.paste} style="flex: 1 0 50%"></textarea>
|
||||||
|
<div id="preview" style="flex: 1 0 50%"></div>
|
||||||
|
</div>
|
||||||
|
${Object.values(this.mentions).map(x => self.render_mention(x))}
|
||||||
|
${this.render_attach_app()}
|
||||||
|
<input type="button" value="Submit" @click=${this.submit}></input>
|
||||||
|
<input type="button" value="Attach" @click=${this.attach}></input>
|
||||||
|
${this.render_attach_app_button()}
|
||||||
|
<input type="button" value="Discard" @click=${this.discard}></input>
|
||||||
|
`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-compose', TfComposeElement);
|
57
apps/cory/ssb/tf-connections.js
Normal file
57
apps/cory/ssb/tf-connections.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfConnectionsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
broadcasts: {type: Array},
|
||||||
|
identities: {type: Array},
|
||||||
|
connections: {type: Array},
|
||||||
|
users: {type: Object},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.broadcasts = [];
|
||||||
|
this.identities = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.users = {};
|
||||||
|
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||||
|
self.identities = identities || [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_emit_change() {
|
||||||
|
let changed_event = new Event('change', {
|
||||||
|
srcElement: this,
|
||||||
|
});
|
||||||
|
this.dispatchEvent(changed_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
||||||
|
this._emit_change();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<h2>Broadcasts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.broadcasts.map(x => html`<li><tf-user id=${x.pubkey} .users=${this.users}></tf-user></li>`)}
|
||||||
|
</ul>
|
||||||
|
<h2>Connections</h2>
|
||||||
|
<ul>
|
||||||
|
${this.connections.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||||
|
</ul>
|
||||||
|
<h2>Local Accounts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-connections', TfConnectionsElement);
|
37
apps/cory/ssb/tf-id-picker.js
Normal file
37
apps/cory/ssb/tf-id-picker.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Provide a list of IDs, and this lets the user pick one.
|
||||||
|
*/
|
||||||
|
class TfIdentityPickerElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
selected: {type: String},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.ids = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
this.dispatchEvent(new Event('change', {
|
||||||
|
srcElement: this,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
|
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
401
apps/cory/ssb/tf-message.js
Normal file
401
apps/cory/ssb/tf-message.js
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
import * as emojis from './emojis.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfMessageElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
message: {type: Object},
|
||||||
|
users: {type: Object},
|
||||||
|
reply: {type: Boolean},
|
||||||
|
raw: {type: Boolean},
|
||||||
|
collapsed: {type: Boolean},
|
||||||
|
content_warning_expanded: {type: Boolean},
|
||||||
|
blog_data: {type: String},
|
||||||
|
blog_expanded: {type: Boolean},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.message = {};
|
||||||
|
this.users = {};
|
||||||
|
this.reply = false;
|
||||||
|
this.raw = false;
|
||||||
|
this.collapsed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_reply() {
|
||||||
|
this.reply = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_votes() {
|
||||||
|
function normalize_expression(expression) {
|
||||||
|
if (expression === 'Like' || !expression) {
|
||||||
|
return '👍';
|
||||||
|
} else if (expression === 'Unlike') {
|
||||||
|
return '👎';
|
||||||
|
} else if (expression === 'heart') {
|
||||||
|
return '❤️';
|
||||||
|
} else {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html`<div>${(this.message.votes || []).map(
|
||||||
|
vote => html`
|
||||||
|
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
||||||
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
|
</span>
|
||||||
|
`)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_raw() {
|
||||||
|
let raw = {
|
||||||
|
id: this.message?.id,
|
||||||
|
previous: this.message?.previous,
|
||||||
|
author: this.message?.author,
|
||||||
|
sequence: this.message?.sequence,
|
||||||
|
timestamp: this.message?.timestamp,
|
||||||
|
hash: this.message?.hash,
|
||||||
|
content: this.message?.content,
|
||||||
|
signature: this.message?.signature,
|
||||||
|
}
|
||||||
|
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
vote(emoji) {
|
||||||
|
let reaction = emoji.emoji;
|
||||||
|
let message = this.message.id;
|
||||||
|
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
||||||
|
tfrpc.rpc.appendMessage(
|
||||||
|
this.whoami,
|
||||||
|
{
|
||||||
|
type: 'vote',
|
||||||
|
vote: {
|
||||||
|
link: message,
|
||||||
|
value: 1,
|
||||||
|
expression: reaction,
|
||||||
|
},
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(error?.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
react(event) {
|
||||||
|
emojis.picker(x => this.vote(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
show_image(link) {
|
||||||
|
let div = document.createElement('div');
|
||||||
|
div.style.left = 0;
|
||||||
|
div.style.top = 0;
|
||||||
|
div.style.width = '100%';
|
||||||
|
div.style.height = '100%';
|
||||||
|
div.style.position = 'fixed';
|
||||||
|
div.style.background = '#000';
|
||||||
|
div.style.zIndex = 100;
|
||||||
|
div.style.display = 'grid';
|
||||||
|
let img = document.createElement('img');
|
||||||
|
img.src = link;
|
||||||
|
img.style.maxWidth = '100%';
|
||||||
|
img.style.maxHeight = '100%';
|
||||||
|
img.style.display = 'block';
|
||||||
|
img.style.margin = 'auto';
|
||||||
|
img.style.objectFit = 'contain';
|
||||||
|
img.style.width = '100%';
|
||||||
|
div.appendChild(img);
|
||||||
|
function image_close(event) {
|
||||||
|
document.body.removeChild(div);
|
||||||
|
window.removeEventListener('keydown', image_close);
|
||||||
|
}
|
||||||
|
div.onclick = image_close;
|
||||||
|
window.addEventListener('keydown', image_close);
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
body_click(event) {
|
||||||
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
|
this.show_image(event.srcElement.src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_mention(mention) {
|
||||||
|
if (!mention?.link || typeof(mention.link) != 'string') {
|
||||||
|
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||||
|
} else if (mention?.link?.startsWith('&') &&
|
||||||
|
mention?.type?.startsWith('image/')) {
|
||||||
|
return html`
|
||||||
|
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
||||||
|
`;
|
||||||
|
} else if (mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('audio:')) {
|
||||||
|
return html`
|
||||||
|
<audio controls style="height: 32px">
|
||||||
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
|
</audio>
|
||||||
|
`;
|
||||||
|
} else if (mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('video:')) {
|
||||||
|
return html`
|
||||||
|
<video controls style="max-height: 240px">
|
||||||
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
} else if (mention.link?.startsWith('&') &&
|
||||||
|
mention?.type === 'application/tildefriends') {
|
||||||
|
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||||
|
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||||
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
||||||
|
} else if (mention.link?.startsWith('#')) {
|
||||||
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
||||||
|
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
||||||
|
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||||
|
} else {
|
||||||
|
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_mentions() {
|
||||||
|
let mentions = this.message?.content?.mentions || [];
|
||||||
|
mentions = mentions.filter(x =>
|
||||||
|
x.name?.startsWith('audio:') ||
|
||||||
|
x.name?.startsWith('video:') ||
|
||||||
|
this.message?.content?.text?.indexOf(x.link) === -1);
|
||||||
|
if (mentions.length) {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
||||||
|
<legend>Mentions</legend>
|
||||||
|
${mentions.map(x => self.render_mention(x))}
|
||||||
|
</fieldset>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_children() {
|
||||||
|
let self = this;
|
||||||
|
if (this.message.child_messages?.length) {
|
||||||
|
if (this.collapsed) {
|
||||||
|
return html`<input type="button" value=${this.message.child_messages?.length + ' More'} @click=${() => self.collapsed = false}></input>`;
|
||||||
|
} else {
|
||||||
|
return html`<input type="button" value="Collapse" @click=${() => self.collapsed = true}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users}></tf-message>`)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let content = this.message?.content;
|
||||||
|
let self = this;
|
||||||
|
let raw_button = this.raw ?
|
||||||
|
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
||||||
|
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
||||||
|
function small_frame(inner) {
|
||||||
|
return html`
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
|
||||||
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
|
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
||||||
|
${raw_button}
|
||||||
|
${self.raw ? self.render_raw() : inner}
|
||||||
|
${self.render_votes()}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
if (this.message.placeholder) {
|
||||||
|
return html`
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
||||||
|
<div>${this.render_votes()}</div>
|
||||||
|
${(this.message.child_messages || []).map(x => html`
|
||||||
|
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} collapsed=true></tf-message>
|
||||||
|
`)}
|
||||||
|
</div>`;
|
||||||
|
} else if (typeof(content?.type === 'string')) {
|
||||||
|
if (content.type == 'about') {
|
||||||
|
let name;
|
||||||
|
let image;
|
||||||
|
let description;
|
||||||
|
if (content.name !== undefined) {
|
||||||
|
name = html`<div><b>Name:</b> ${content.name}</div>`;
|
||||||
|
}
|
||||||
|
if (content.image !== undefined) {
|
||||||
|
image = html`
|
||||||
|
<div><img src=${'/' + content.image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (content.description !== undefined) {
|
||||||
|
description = html`
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return small_frame(html`
|
||||||
|
<div style="font-weight: bold">Updated profile.</div>
|
||||||
|
${name}
|
||||||
|
${image}
|
||||||
|
${description}
|
||||||
|
`);
|
||||||
|
} else if (content.type == 'contact') {
|
||||||
|
return small_frame(html`
|
||||||
|
<div>
|
||||||
|
is
|
||||||
|
${
|
||||||
|
content.blocking === true ? 'blocking' :
|
||||||
|
content.blocking === false ? 'no longer blocking' :
|
||||||
|
content.following === true ? 'following' :
|
||||||
|
content.following === false ? 'no longer following' :
|
||||||
|
'?'
|
||||||
|
}
|
||||||
|
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else if (content.type == 'post') {
|
||||||
|
let reply = this.reply ? html`
|
||||||
|
<tf-compose
|
||||||
|
?enabled=${this.reply}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${this.message.content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
@tf-discard=${() => this.reply = false}></tf-compose>
|
||||||
|
` : html`
|
||||||
|
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
||||||
|
`;
|
||||||
|
let self = this;
|
||||||
|
let body = this.raw ?
|
||||||
|
this.render_raw() :
|
||||||
|
unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
let content_warning = html`
|
||||||
|
<div style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px" @click=${x => self.content_warning_expanded = !self.content_warning_expanded}>${content.contentWarning}</div>
|
||||||
|
`;
|
||||||
|
let content_html =
|
||||||
|
html`
|
||||||
|
<div @click=${this.body_click}>${body}</div>
|
||||||
|
${this.render_mentions()}
|
||||||
|
`;
|
||||||
|
let payload =
|
||||||
|
content.contentWarning ?
|
||||||
|
self.content_warning_expanded ?
|
||||||
|
html`
|
||||||
|
${content_warning}
|
||||||
|
${content_html}
|
||||||
|
` :
|
||||||
|
content_warning :
|
||||||
|
content_html;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
|
<span>${raw_button}</span>
|
||||||
|
</div>
|
||||||
|
${payload}
|
||||||
|
${this.render_votes()}
|
||||||
|
<div>
|
||||||
|
${reply}
|
||||||
|
<input type="button" value="React" @click=${this.react}></input>
|
||||||
|
</div>
|
||||||
|
${this.render_children()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (content.type === 'blog') {
|
||||||
|
let self = this;
|
||||||
|
console.log('requesting data');
|
||||||
|
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
||||||
|
self.blog_data = data;
|
||||||
|
});
|
||||||
|
let payload =
|
||||||
|
this.blog_expanded ?
|
||||||
|
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
||||||
|
undefined;
|
||||||
|
let body = this.raw ?
|
||||||
|
this.render_raw() :
|
||||||
|
html`
|
||||||
|
<div
|
||||||
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
|
@click=${x => self.blog_expanded = !self.blog_expanded}>
|
||||||
|
<h2>${content.title}</h2>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<img src=/${content.thumbnail}/view></img>
|
||||||
|
<span>${content.summary}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${payload}
|
||||||
|
`;
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
|
<span>${raw_button}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>${body}</div>
|
||||||
|
${this.render_mentions()}
|
||||||
|
${this.render_votes()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (content.type === 'pub') {
|
||||||
|
return small_frame(html`
|
||||||
|
<span>
|
||||||
|
<div>
|
||||||
|
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
||||||
|
</div>
|
||||||
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
|
</span>`);
|
||||||
|
} else if (content.type === 'channel') {
|
||||||
|
return small_frame(html`
|
||||||
|
<div>
|
||||||
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} else if (typeof(this.message.content) == 'string') {
|
||||||
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
} else {
|
||||||
|
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return small_frame(this.render_raw());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-message', TfMessageElement);
|
159
apps/cory/ssb/tf-news.js
Normal file
159
apps/cory/ssb/tf-news.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfNewsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
messages: {type: Array},
|
||||||
|
following: {type: Array},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.messages = [];
|
||||||
|
this.following = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
process_messages(messages) {
|
||||||
|
let self = this;
|
||||||
|
let messages_by_id = {};
|
||||||
|
|
||||||
|
console.log('processing', messages.length, 'messages');
|
||||||
|
|
||||||
|
function ensure_message(id) {
|
||||||
|
let found = messages_by_id[id];
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
} else {
|
||||||
|
let added = {
|
||||||
|
id: id,
|
||||||
|
placeholder: true,
|
||||||
|
content: '"placeholder"',
|
||||||
|
parent_message: undefined,
|
||||||
|
child_messages: [],
|
||||||
|
votes: [],
|
||||||
|
};
|
||||||
|
messages_by_id[id] = added;
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function link_message(message) {
|
||||||
|
if (message.content.type === 'vote') {
|
||||||
|
let parent = ensure_message(message.content.vote.link);
|
||||||
|
if (!parent.votes) {
|
||||||
|
parent.votes = [];
|
||||||
|
}
|
||||||
|
parent.votes.push(message);
|
||||||
|
message.parent_message = message.content.vote.link;
|
||||||
|
} else if (message.content.type == 'post') {
|
||||||
|
if (message.content.root) {
|
||||||
|
if (typeof(message.content.root) === 'string') {
|
||||||
|
let m = ensure_message(message.content.root);
|
||||||
|
if (!m.child_messages) {
|
||||||
|
m.child_messages = [];
|
||||||
|
}
|
||||||
|
m.child_messages.push(message);
|
||||||
|
message.parent_message = message.content.root;
|
||||||
|
} else {
|
||||||
|
let m = ensure_message(message.content.root[0]);
|
||||||
|
if (!m.child_messages) {
|
||||||
|
m.child_messages = [];
|
||||||
|
}
|
||||||
|
m.child_messages.push(message);
|
||||||
|
message.parent_message = message.content.root[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let message of messages) {
|
||||||
|
message.parent_message = undefined;
|
||||||
|
message.child_messages = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let message of messages) {
|
||||||
|
try {
|
||||||
|
message.content = JSON.parse(message.content);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (!messages_by_id[message.id]) {
|
||||||
|
messages_by_id[message.id] = message;
|
||||||
|
link_message(message);
|
||||||
|
} else if (messages_by_id[message.id].placeholder) {
|
||||||
|
let placeholder = messages_by_id[message.id];
|
||||||
|
messages_by_id[message.id] = message;
|
||||||
|
message.parent_message = placeholder.parent_message;
|
||||||
|
message.child_messages = placeholder.child_messages;
|
||||||
|
message.votes = placeholder.votes;
|
||||||
|
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
||||||
|
let children = messages_by_id[placeholder.parent_message].child_messages;
|
||||||
|
children.splice(children.indexOf(placeholder), 1);
|
||||||
|
children.push(message);
|
||||||
|
}
|
||||||
|
link_message(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages_by_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_latest_subtree_timestamp(messages) {
|
||||||
|
let latest = 0;
|
||||||
|
for (let message of messages || []) {
|
||||||
|
if (message.latest_subtree_timestamp === undefined) {
|
||||||
|
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
||||||
|
}
|
||||||
|
latest = Math.max(latest, message.latest_subtree_timestamp);
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize_messages(messages_by_id) {
|
||||||
|
function recursive_sort(messages, top) {
|
||||||
|
if (messages) {
|
||||||
|
if (top) {
|
||||||
|
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
|
||||||
|
} else {
|
||||||
|
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
}
|
||||||
|
for (let message of messages) {
|
||||||
|
recursive_sort(message.child_messages, false);
|
||||||
|
}
|
||||||
|
return messages.map(x => Object.assign({}, x));
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roots = Object.values(messages_by_id).filter(x => !x.parent_message);
|
||||||
|
this.update_latest_subtree_timestamp(roots);
|
||||||
|
return recursive_sort(roots, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_and_render(messages) {
|
||||||
|
let messages_by_id = this.process_messages(messages);
|
||||||
|
let final_messages = this.finalize_messages(messages_by_id);
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} collapsed=true></tf-message>`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let messages = this.load_and_render(this.messages || []);
|
||||||
|
return html`${until(messages, html`<div>Loading placeholders...</div>`)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-news', TfNewsElement);
|
180
apps/cory/ssb/tf-profile.js
Normal file
180
apps/cory/ssb/tf-profile.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfProfileElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
editing: {type: Object},
|
||||||
|
whoami: {type: String},
|
||||||
|
id: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
size: {type: Number},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.editing = null;
|
||||||
|
this.whoami = null;
|
||||||
|
this.id = null;
|
||||||
|
this.users = {};
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
modify(change) {
|
||||||
|
tfrpc.rpc.appendMessage(this.whoami,
|
||||||
|
Object.assign({
|
||||||
|
type: 'contact',
|
||||||
|
contact: this.id,
|
||||||
|
}, change)).catch(function(error) {
|
||||||
|
alert(error?.message);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
follow() {
|
||||||
|
this.modify({following: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
unfollow() {
|
||||||
|
this.modify({following: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
block() {
|
||||||
|
this.modify({blocking: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
unblock() {
|
||||||
|
this.modify({blocking: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
let original = this.users[this.id];
|
||||||
|
this.editing = {
|
||||||
|
name: original.name,
|
||||||
|
description: original.description,
|
||||||
|
image: original.image,
|
||||||
|
};
|
||||||
|
console.log(this.editing);
|
||||||
|
}
|
||||||
|
|
||||||
|
save_edits() {
|
||||||
|
let self = this;
|
||||||
|
let message = {
|
||||||
|
type: 'about',
|
||||||
|
about: this.whoami,
|
||||||
|
};
|
||||||
|
for (let key of Object.keys(this.editing)) {
|
||||||
|
if (this.editing[key] !== this.users[this.id][key]) {
|
||||||
|
message[key] = this.editing[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
|
self.editing = null;
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(error?.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discard_edits() {
|
||||||
|
this.editing = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
attach_image() {
|
||||||
|
let self = this;
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.onchange = function(event) {
|
||||||
|
let file = event.target.files[0];
|
||||||
|
file.arrayBuffer().then(function(buffer) {
|
||||||
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
|
return tfrpc.rpc.store_blob(bin);
|
||||||
|
}).then(function(id) {
|
||||||
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
|
console.log(self.editing);
|
||||||
|
}).catch(function(e) {
|
||||||
|
alert(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
let profile = this.users[this.id] || {};
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||||
|
[this.id]).then(function(result) {
|
||||||
|
self.size = result[0].size;
|
||||||
|
});
|
||||||
|
let edit;
|
||||||
|
let follow;
|
||||||
|
let block;
|
||||||
|
if (this.id === this.whoami) {
|
||||||
|
if (this.editing) {
|
||||||
|
edit = html`
|
||||||
|
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
|
||||||
|
<input type="button" value="Discard" @click=${this.discard_edits}></input>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.id !== this.whoami &&
|
||||||
|
this.users[this.whoami]?.following) {
|
||||||
|
follow =
|
||||||
|
this.users[this.whoami].following[this.id] ?
|
||||||
|
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
|
||||||
|
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
|
||||||
|
}
|
||||||
|
if (this.id !== this.whoami &&
|
||||||
|
this.users[this.whoami]?.blocking) {
|
||||||
|
block =
|
||||||
|
this.users[this.whoami].blocking[this.id] ?
|
||||||
|
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
|
||||||
|
html`<input type="button" value="Block" @click=${this.block}></input>`;
|
||||||
|
}
|
||||||
|
let edit_profile = this.editing ? html`
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
<div>
|
||||||
|
<label for="name">Name:</label>
|
||||||
|
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><label for="description">Description:</label></div>
|
||||||
|
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
||||||
|
</div>
|
||||||
|
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
||||||
|
</div>` : null;
|
||||||
|
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
||||||
|
image = this.editing?.image ?? image;
|
||||||
|
let description = this.editing?.description ?? profile.description;
|
||||||
|
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||||
|
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
${edit_profile}
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
|
<div>${unsafeHTML(tfutils.markdown(description))}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Following ${Object.keys(profile.following || {}).length} identities.
|
||||||
|
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
|
||||||
|
Blocking ${Object.keys(profile.blocking || {}).length} identities.
|
||||||
|
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
${edit}
|
||||||
|
${follow}
|
||||||
|
${block}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-profile', TfProfileElement);
|
32
apps/cory/ssb/tf-styles.js
Normal file
32
apps/cory/ssb/tf-styles.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {css} from './lit-all.min.js';
|
||||||
|
|
||||||
|
export let styles = css`
|
||||||
|
a:link {
|
||||||
|
color: #bbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #ddf;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: min(640px, 100%);
|
||||||
|
max-height: min(480px, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
border: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:disabled {
|
||||||
|
color: #088;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
`;
|
103
apps/cory/ssb/tf-tab-connections.js
Normal file
103
apps/cory/ssb/tf-tab-connections.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfTabConnectionsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
broadcasts: {type: Array},
|
||||||
|
identities: {type: Array},
|
||||||
|
connections: {type: Array},
|
||||||
|
users: {type: Object},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.broadcasts = [];
|
||||||
|
this.identities = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.users = {};
|
||||||
|
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||||
|
self.identities = identities || [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render_connection_summary(connection) {
|
||||||
|
if (connection.address && connection.port) {
|
||||||
|
return html`(<small>${connection.address}:${connection.port}</small>)`;
|
||||||
|
} else if (connection.tunnel) {
|
||||||
|
return html`(room peer)`;
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_room_peers(connection) {
|
||||||
|
let self = this;
|
||||||
|
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
||||||
|
if (peers.length) {
|
||||||
|
return html`
|
||||||
|
<ul>
|
||||||
|
${peers.map(x => html`${self.render_room_peer(x)}`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _tunnel(portal, target) {
|
||||||
|
let request_number = await tfrpc.rpc.connectionSendJson(portal, {name: ['tunnel', 'connect'], args: [{portal: portal, target: target}], type: 'duplex'});
|
||||||
|
return tfrpc.rpc.createTunnel(portal, request_number, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_room_peer(connection) {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<li>
|
||||||
|
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
||||||
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_broadcast(connection) {
|
||||||
|
return html`
|
||||||
|
<li>
|
||||||
|
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
|
||||||
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
|
${this.render_connection_summary(connection)}
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<h2>New Connection</h2>
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
<textarea id="code"></textarea>
|
||||||
|
</div>
|
||||||
|
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
|
||||||
|
<h2>Broadcasts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
||||||
|
</ul>
|
||||||
|
<h2>Connections</h2>
|
||||||
|
<ul>
|
||||||
|
${this.connections.map(x => html`
|
||||||
|
<li>
|
||||||
|
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
${self.render_room_peers(x)}
|
||||||
|
</li>
|
||||||
|
`)}
|
||||||
|
</ul>
|
||||||
|
<h2>Local Accounts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-connections', TfTabConnectionsElement);
|
174
apps/cory/ssb/tf-tab-news.js
Normal file
174
apps/cory/ssb/tf-tab-news.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabNewsFeedElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
hash: {type: String},
|
||||||
|
following: {type: Array},
|
||||||
|
messages: {type: Array},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.hash = '#';
|
||||||
|
this.following = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_messages() {
|
||||||
|
if (this.hash.startsWith('#@')) {
|
||||||
|
let r = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH mine AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
WHERE messages.author = ?
|
||||||
|
ORDER BY sequence DESC
|
||||||
|
LIMIT 20)
|
||||||
|
SELECT messages.*
|
||||||
|
FROM mine
|
||||||
|
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT * FROM mine
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
this.hash.substring(1),
|
||||||
|
]);
|
||||||
|
return r;
|
||||||
|
} else if (this.hash.startsWith('#%')) {
|
||||||
|
return await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
WHERE id = ?1
|
||||||
|
UNION
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages JOIN messages_refs
|
||||||
|
ON messages.id = messages_refs.message
|
||||||
|
WHERE messages_refs.ref = ?1
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
this.hash.substring(1),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH news AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.timestamp > ?
|
||||||
|
ORDER BY messages.timestamp DESC)
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT news.* FROM news
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
new Date().valueOf() - 24 * 60 * 60 * 1000,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.messages ||
|
||||||
|
this._messages_hash !== this.hash ||
|
||||||
|
this._messages_following !== this.following) {
|
||||||
|
console.log(`loading messages for ${this.whoami}`);
|
||||||
|
let self = this;
|
||||||
|
this.messages = [];
|
||||||
|
this._messages_hash = this.hash;
|
||||||
|
this._messages_following = this.following;
|
||||||
|
this.fetch_messages().then(function(messages) {
|
||||||
|
self.messages = messages;
|
||||||
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(JSON.stringify(error, null, 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following}></tf-news>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TfTabNewsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
hash: {type: String},
|
||||||
|
unread: {type: Array},
|
||||||
|
following: {type: Array},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.hash = '#';
|
||||||
|
this.unread = [];
|
||||||
|
this.following = [];
|
||||||
|
this.cache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
show_more() {
|
||||||
|
let unread = this.unread;
|
||||||
|
let news = this.renderRoot?.getElementById('news');
|
||||||
|
if (news) {
|
||||||
|
console.log('injecting messages', news.messages);
|
||||||
|
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
||||||
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_messages_text() {
|
||||||
|
if (!this.unread?.length) {
|
||||||
|
return 'No new messages.';
|
||||||
|
}
|
||||||
|
let counts = {};
|
||||||
|
for (let message of this.unread) {
|
||||||
|
let type = 'private';
|
||||||
|
try {
|
||||||
|
type = JSON.parse(message.content).type || type;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
|
}
|
||||||
|
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let profile = this.hash.startsWith('#@') ?
|
||||||
|
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||||
|
return html`
|
||||||
|
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
|
||||||
|
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
|
||||||
|
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
||||||
|
<div><tf-compose whoami=${this.whoami} .users=${this.users}></tf-compose></div>
|
||||||
|
${profile}
|
||||||
|
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash}></tf-tab-news-feed>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
||||||
|
customElements.define('tf-tab-news', TfTabNewsElement);
|
73
apps/cory/ssb/tf-tab-search.js
Normal file
73
apps/cory/ssb/tf-tab-search.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabSearchElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
query: {type: String},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
console.log('Searching...', this.whoami, query);
|
||||||
|
let search = this.renderRoot.getElementById('search');
|
||||||
|
if (search ) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
search.select();
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
|
let results = await tfrpc.rpc.query(`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
ORDER BY timestamp DESC limit 100
|
||||||
|
`,
|
||||||
|
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
|
||||||
|
console.log('Done.');
|
||||||
|
search = this.renderRoot.getElementById('search');
|
||||||
|
if (search ) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
search.select();
|
||||||
|
}
|
||||||
|
this.renderRoot.getElementById('news').messages = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
search_keydown(event) {
|
||||||
|
if (event.keyCode == 13) {
|
||||||
|
this.query = this.renderRoot.getElementById('search').value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.query !== this.last_query) {
|
||||||
|
this.search(this.query);
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
|
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
||||||
|
</div>
|
||||||
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-search', TfTabSearchElement);
|
39
apps/cory/ssb/tf-user.js
Normal file
39
apps/cory/ssb/tf-user.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfUserElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
id: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.id = null;
|
||||||
|
this.users = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.users[this.id]) {
|
||||||
|
let image = this.users[this.id].image;
|
||||||
|
image = typeof(image) == 'string' ? image : image?.link;
|
||||||
|
return html`
|
||||||
|
<div style="display: inline-block; font-weight: bold">
|
||||||
|
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
||||||
|
<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
|
||||||
|
<a target="_top" href=${'#' + this.id}>${this.id}</a>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-user', TfUserElement);
|
48
apps/cory/ssb/tf-utils.js
Normal file
48
apps/cory/ssb/tf-utils.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
parsed = hashtagify.transform(parsed);
|
||||||
|
parsed = linkify.transform(parsed);
|
||||||
|
var walker = parsed.walker();
|
||||||
|
var event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.type == 'link') {
|
||||||
|
if (node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
} else if (node.type == 'image') {
|
||||||
|
if (node.destination.startsWith('&')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function human_readable_size(bytes) {
|
||||||
|
let v = bytes;
|
||||||
|
let u = 'B';
|
||||||
|
for (let unit of ['kB', 'MB', 'GB']) {
|
||||||
|
if (v > 1024) {
|
||||||
|
v /= 1024;
|
||||||
|
u = unit;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.round(v * 10) / 10} ${u}`;
|
||||||
|
}
|
32
apps/cory/ssb/tribute.css
Normal file
32
apps/cory/ssb/tribute.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
.tribute-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: auto;
|
||||||
|
overflow: auto;
|
||||||
|
display: block;
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
.tribute-container ul {
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
background: #efefef;
|
||||||
|
}
|
||||||
|
.tribute-container li {
|
||||||
|
padding: 5px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.tribute-container li.highlight {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
.tribute-container li span {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tribute-container li.no-match {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.tribute-container .menu-highlighted {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
1797
apps/cory/ssb/tribute.esm.js
Normal file
1797
apps/cory/ssb/tribute.esm.js
Normal file
File diff suppressed because it is too large
Load Diff
1
apps/cory/todo.json
Normal file
1
apps/cory/todo.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"type":"tildefriends-app","files":{"app.js":"&QUR1tKa15B5Or8AfPX/8Zs87teSeX0Mh/HF7PEPBom0=.sha256","index.html":"&QXhwvxhHc9fa8iL6088hGDu9FgWdY7wkXgvU2BMNv0A=.sha256","lit-core.min.js":"&tP9KhbgwF1chFqPtkNZ12Yx9AfkpnSjFiPcX5Pw5J9g=.sha256","script.js":"&KgOaUVjBM4MzSy7PpUVQHETuvgXAx2JGPJABksBg+QY=.sha256"}}
|
82
apps/cory/todo/app.js
Normal file
82
apps/cory/todo/app.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_db;
|
||||||
|
|
||||||
|
tfrpc.register(async function todo_get_all() {
|
||||||
|
let names = await todo_get_names();
|
||||||
|
let result = [];
|
||||||
|
for (let name of names) {
|
||||||
|
result.push({
|
||||||
|
name: name,
|
||||||
|
items: await todo_get(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function todo_get_names() {
|
||||||
|
return JSON.parse((await g_db.get('files')) ?? '[]');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function todo_add(list) {
|
||||||
|
let exchanged = false;
|
||||||
|
let tries = 10;
|
||||||
|
while (!exchanged && tries-- > 0) {
|
||||||
|
let original = await g_db.get('files');
|
||||||
|
let names = JSON.parse(original ?? '[]');
|
||||||
|
let set = new Set(names);
|
||||||
|
set.add(list);
|
||||||
|
names = JSON.stringify([...set].sort());
|
||||||
|
exchanged = original === names || await g_db.exchange('files', original, names);
|
||||||
|
}
|
||||||
|
return exchanged;
|
||||||
|
}
|
||||||
|
tfrpc.register(todo_add);
|
||||||
|
|
||||||
|
async function todo_remove(list) {
|
||||||
|
let exchanged = false;
|
||||||
|
let tries = 10;
|
||||||
|
while (!exchanged && tries-- > 0) {
|
||||||
|
let original = await g_db.get('files');
|
||||||
|
let names = JSON.parse(original ?? '[]');
|
||||||
|
let set = new Set(names);
|
||||||
|
set.delete(list);
|
||||||
|
names = JSON.stringify([...set].sort());
|
||||||
|
exchanged = original === names || await g_db.exchange('files', original, names);
|
||||||
|
}
|
||||||
|
await g_db.remove('list:' + list);
|
||||||
|
return exchanged;
|
||||||
|
}
|
||||||
|
tfrpc.register(todo_remove);
|
||||||
|
|
||||||
|
tfrpc.register(async function todo_rename(old_name, new_name) {
|
||||||
|
if (await g_db.get('list:' + new_name)) {
|
||||||
|
throw RuntimeError(`${new_name} already exists.`);
|
||||||
|
}
|
||||||
|
let list = await todo_get(old_name);
|
||||||
|
await todo_set(new_name, list);
|
||||||
|
await todo_add(new_name);
|
||||||
|
await todo_remove(old_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function todo_get(list) {
|
||||||
|
try {
|
||||||
|
let value = await g_db.get('list:' + list);
|
||||||
|
return JSON.parse(value ?? '[]');
|
||||||
|
} catch (error) {
|
||||||
|
print(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function todo_set(list, value) {
|
||||||
|
await g_db.set('list:' + list, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
tfrpc.register(todo_set);
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
g_db = await database('todo');
|
||||||
|
await app.setDocument(utf8Decode(getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
11
apps/cory/todo/index.html
Normal file
11
apps/cory/todo/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>TODO</title>
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff">
|
||||||
|
<h1>TODO</h1>
|
||||||
|
<tf-todos></tf-todos>
|
||||||
|
</body>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</html>
|
29
apps/cory/todo/lit-core.min.js
vendored
Normal file
29
apps/cory/todo/lit-core.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
187
apps/cory/todo/script.js
Normal file
187
apps/cory/todo/script.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import {LitElement, html} from './lit-core.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TodosElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
lists: {type: Array}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.lists = [];
|
||||||
|
let self = this;
|
||||||
|
tfrpc.rpc.todo_get_all().then(function(lists) {
|
||||||
|
self.lists = lists;
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async new_list() {
|
||||||
|
await tfrpc.rpc.todo_add('new list');
|
||||||
|
await this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
this.lists = await tfrpc.rpc.todo_get_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<div style="display: flex">
|
||||||
|
${this.lists.map(x => html`
|
||||||
|
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
<input type="button" @click=${this.new_list} value="+ List"></input>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodoListElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
name: {type: String},
|
||||||
|
items: {type: Array},
|
||||||
|
editing: {type: Number},
|
||||||
|
editing_name: {type: Boolean},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
let self = this;
|
||||||
|
console.log('saving', self.name, self.items);
|
||||||
|
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
|
||||||
|
console.log('saved', self.name, self.items);
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_item(item) {
|
||||||
|
let index = this.items.indexOf(item);
|
||||||
|
this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1));
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_check(event, item) {
|
||||||
|
item.x = event.srcElement.checked;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
input_blur(item) {
|
||||||
|
this.save();
|
||||||
|
this.editing = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
input_change(event, item) {
|
||||||
|
item.text = event.srcElement.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
input_keydown(event, item) {
|
||||||
|
if (event.key === 'Enter' || event.key === 'Escape') {
|
||||||
|
item.text = event.srcElement.value;
|
||||||
|
this.editing = undefined;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
if (edit) {
|
||||||
|
edit.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_item(item) {
|
||||||
|
let index = this.items.indexOf(item);
|
||||||
|
let self = this;
|
||||||
|
if (index === this.editing) {
|
||||||
|
return html`
|
||||||
|
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
||||||
|
<input
|
||||||
|
id="edit"
|
||||||
|
type="text"
|
||||||
|
value=${item.text}
|
||||||
|
@change=${event => self.input_change(event, item)}
|
||||||
|
@keydown=${event => self.input_keydown(event, item)}
|
||||||
|
@blur=${x => self.input_blur(item)}></input>
|
||||||
|
<span @click=${x => self.remove_item(item)}>x</span></div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
||||||
|
<span @click=${x => self.editing = index}>${item.text}</span>
|
||||||
|
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_item() {
|
||||||
|
this.items = [].concat(this.items || [], [{text: 'new item'}]);
|
||||||
|
this.editing = this.items.length - 1;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove_list() {
|
||||||
|
if (confirm(`Are you sure you want to remove "${this.name}"?`)) {
|
||||||
|
await tfrpc.rpc.todo_remove(this.name);
|
||||||
|
this.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rename(new_name) {
|
||||||
|
let self = this;
|
||||||
|
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
|
||||||
|
self.dispatchEvent(new Event('change'));
|
||||||
|
self.editing_name = false;
|
||||||
|
}).catch(function(error) {
|
||||||
|
console.log(error);
|
||||||
|
alert(error.message);
|
||||||
|
self.editing_name = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
name_blur(new_name) {
|
||||||
|
this.rename(new_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
name_keydown(event, item) {
|
||||||
|
let self = this;
|
||||||
|
if (event.key == 'Enter' || event.key === 'Escape') {
|
||||||
|
let new_name = event.srcElement.value;
|
||||||
|
this.rename(new_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
let name = this.editing_name ?
|
||||||
|
html`<input
|
||||||
|
type="text"
|
||||||
|
id="edit"
|
||||||
|
@keydown=${event => self.name_keydown(event)}
|
||||||
|
@blur=${event => self.name_blur(event.srcElement.value)}
|
||||||
|
value=${this.name}></input>` :
|
||||||
|
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`;
|
||||||
|
return html`
|
||||||
|
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
||||||
|
${name}
|
||||||
|
${(this.items || []).map(x => self.render_item(x))}
|
||||||
|
<button @click=${self.add_item}>+ Item</button>
|
||||||
|
<button @click=${self.remove_list}>- List</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-todo-list', TodoListElement);
|
||||||
|
customElements.define('tf-todos', TodosElement);
|
147
core/app.js
147
core/app.js
@ -1,4 +1,14 @@
|
|||||||
"use strict";
|
import * as auth from './auth.js';
|
||||||
|
import * as core from './core.js';
|
||||||
|
|
||||||
|
let g_next_id = 1;
|
||||||
|
let g_calls = {};
|
||||||
|
|
||||||
|
let gSessionIndex = 0;
|
||||||
|
|
||||||
|
function makeSessionId() {
|
||||||
|
return (gSessionIndex++).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
this._on_output = null;
|
this._on_output = null;
|
||||||
@ -12,35 +22,63 @@ App.prototype.readOutput = function(callback) {
|
|||||||
|
|
||||||
App.prototype.makeFunction = function(api) {
|
App.prototype.makeFunction = function(api) {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
let id = g_next_id++;
|
||||||
|
while (!id || g_calls[id]) {
|
||||||
|
id = g_next_id++;
|
||||||
|
}
|
||||||
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
g_calls[id] = {resolve: resolve, reject: reject};
|
||||||
|
});
|
||||||
let result = function() {
|
let result = function() {
|
||||||
let message = {action: api[0]};
|
let message = {
|
||||||
for (let i = 1; i < api.length; i++) {
|
message: 'tfrpc',
|
||||||
message[api[i]] = arguments[i - 1];
|
method: api[0],
|
||||||
}
|
params: [...arguments],
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
self.send(message);
|
self.send(message);
|
||||||
|
return promise;
|
||||||
};
|
};
|
||||||
Object.defineProperty(result, 'name', {value: api[0], writable: false});
|
Object.defineProperty(result, 'name', {value: api[0], writable: false});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
App.prototype.send = function(message) {
|
App.prototype.send = function(message) {
|
||||||
if (message) {
|
if (this._send_queue) {
|
||||||
this._send_queue.push(message);
|
if (this._on_output) {
|
||||||
|
this._send_queue.forEach(x => this._on_output(x));
|
||||||
|
this._send_queue = null;
|
||||||
|
} else if (message) {
|
||||||
|
this._send_queue.push(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this._on_output) {
|
if (message && this._on_output) {
|
||||||
this._send_queue.forEach(message => this._on_output(message));
|
this._on_output(message);
|
||||||
this._send_queue = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function socket(request, response, client) {
|
function socket(request, response, client) {
|
||||||
var process;
|
let process;
|
||||||
var options = {};
|
let options = {};
|
||||||
var credentials = auth.query(request.headers);
|
let credentials = auth.query(request.headers);
|
||||||
|
let refresh_token = credentials?.refresh?.token;
|
||||||
|
let refresh_interval = credentials?.refresh?.interval;
|
||||||
|
|
||||||
|
response.onClose = async function() {
|
||||||
|
if (process && process.task) {
|
||||||
|
process.task.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.onError = async function(error) {
|
||||||
|
if (process && process.task) {
|
||||||
|
process.task.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.onMessage = async function(event) {
|
response.onMessage = async function(event) {
|
||||||
if (event.opCode == 0x1 || event.opCode == 0x2) {
|
if (event.opCode == 0x1 || event.opCode == 0x2) {
|
||||||
var message;
|
let message;
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(event.data);
|
message = JSON.parse(event.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -48,28 +86,48 @@ function socket(request, response, client) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.action == "hello") {
|
if (message.action == "hello") {
|
||||||
var packageOwner;
|
let packageOwner;
|
||||||
var packageName;
|
let packageName;
|
||||||
var blobId;
|
let blobId;
|
||||||
var match;
|
let match;
|
||||||
if (match = /^\/(&[^\.]*\.\w+)(\/?.*)/.exec(message.path)) {
|
let parentApp;
|
||||||
|
if (match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path)) {
|
||||||
blobId = match[1];
|
blobId = match[1];
|
||||||
} else if (match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path)) {
|
} else if (match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path)) {
|
||||||
var user = match[1];
|
packageOwner = match[1];
|
||||||
var path = match[2];
|
packageName = match[2];
|
||||||
blobId = await new Database(user).get('path:' + path);
|
blobId = await new Database(packageOwner).get('path:' + packageName);
|
||||||
if (!blobId) {
|
if (!blobId) {
|
||||||
response.send(JSON.stringify({action: "error", error: message.path + ' not found'}), 0x1);
|
response.send(JSON.stringify({
|
||||||
request.close();
|
message: 'tfrpc',
|
||||||
|
method: "error",
|
||||||
|
params: [message.path + ' not found'],
|
||||||
|
id: -1,
|
||||||
|
}), 0x1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (packageOwner != 'core') {
|
||||||
|
let coreId = await new Database('core').get('path:' + packageName);
|
||||||
|
parentApp = {
|
||||||
|
path: '/~core/' + packageName + '/',
|
||||||
|
id: coreId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.send(JSON.stringify({action: "session", credentials: credentials}), 0x1);
|
response.send(JSON.stringify({
|
||||||
|
action: "session",
|
||||||
|
credentials: credentials,
|
||||||
|
parentApp: parentApp,
|
||||||
|
id: blobId,
|
||||||
|
}), 0x1);
|
||||||
|
|
||||||
options.api = message.api || [];
|
options.api = message.api || [];
|
||||||
options.credentials = credentials;
|
options.credentials = credentials;
|
||||||
var sessionId = makeSessionId();
|
options.packageOwner = packageOwner;
|
||||||
|
options.packageName = packageName;
|
||||||
|
let sessionId = makeSessionId();
|
||||||
if (blobId) {
|
if (blobId) {
|
||||||
process = await getSessionProcessBlob(blobId, sessionId, options);
|
process = await core.getSessionProcessBlob(blobId, sessionId, options);
|
||||||
}
|
}
|
||||||
if (process) {
|
if (process) {
|
||||||
process.app.readOutput(function(message) {
|
process.app.readOutput(function(message) {
|
||||||
@ -78,9 +136,9 @@ function socket(request, response, client) {
|
|||||||
process.app.send();
|
process.app.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
var ping = function() {
|
let ping = function() {
|
||||||
var now = Date.now();
|
let now = Date.now();
|
||||||
var again = true;
|
let again = true;
|
||||||
if (now - process.lastActive < process.timeout) {
|
if (now - process.lastActive < process.timeout) {
|
||||||
// Active.
|
// Active.
|
||||||
} else if (process.lastPing > process.lastActive) {
|
} else if (process.lastPing > process.lastActive) {
|
||||||
@ -103,14 +161,31 @@ function socket(request, response, client) {
|
|||||||
if (process && process.timeout > 0) {
|
if (process && process.timeout > 0) {
|
||||||
setTimeout(ping, process.timeout);
|
setTimeout(ping, process.timeout);
|
||||||
}
|
}
|
||||||
|
} else if (message.action == 'enableStats') {
|
||||||
|
if (process) {
|
||||||
|
core.enableStats(process, message.enabled);
|
||||||
|
}
|
||||||
|
} else if (message.action == 'resetPermission') {
|
||||||
|
if (process) {
|
||||||
|
process.resetPermission(message.permission);
|
||||||
|
}
|
||||||
|
} else if (message.message == 'tfrpc') {
|
||||||
|
if (message.id && g_calls[message.id]) {
|
||||||
|
if (message.error !== undefined) {
|
||||||
|
g_calls[message.id].reject(message.error);
|
||||||
|
} else {
|
||||||
|
g_calls[message.id].resolve(message.result);
|
||||||
|
}
|
||||||
|
delete g_calls[message.id];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (process && process.eventHandlers['message']) {
|
if (process && process.eventHandlers['message']) {
|
||||||
await invoke(process.eventHandlers['message'], [message]);
|
await core.invoke(process.eventHandlers['message'], [message]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event.opCode == 0x8) {
|
} else if (event.opCode == 0x8) {
|
||||||
// Close.
|
// Close.
|
||||||
if (process) {
|
if (process && process.task) {
|
||||||
process.task.kill();
|
process.task.kill();
|
||||||
}
|
}
|
||||||
response.send(event.data, 0x8);
|
response.send(event.data, 0x8);
|
||||||
@ -122,6 +197,12 @@ function socket(request, response, client) {
|
|||||||
process.lastActive = Date.now();
|
process.lastActive = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (refresh_token) {
|
||||||
|
return {
|
||||||
|
'Set-Cookie': `session=${refresh_token}; path=/; Max-Age=${refresh_interval}; Secure; SameSite=Strict`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.socket = socket;
|
export { socket, App };
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Auth</title>
|
<title>Tilde Friends Sign-in</title>
|
||||||
<script>
|
<script>
|
||||||
function showHideConfirm() {
|
function showHideConfirm() {
|
||||||
document.getElementById("confirmPassword").style.display = document.getElementById("register").checked ? "block" : "none";
|
document.getElementById("confirmPassword").style.display = document.getElementById("register").checked ? "block" : "none";
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<!--HEAD-->
|
<!--HEAD-->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Login</h1>
|
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
|
||||||
<div id="content"><!--SESSION--></div>
|
<div id="content"><!--SESSION--></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
259
core/auth.js
259
core/auth.js
@ -1,44 +1,74 @@
|
|||||||
"use strict";
|
import * as core from './core.js';
|
||||||
|
import * as http from './http.js';
|
||||||
|
import * as form from './form.js';
|
||||||
|
|
||||||
var gTokens = {};
|
var gTokens = {};
|
||||||
|
|
||||||
var form = require('form');
|
|
||||||
var http = require('http');
|
|
||||||
|
|
||||||
var gDatabase = new Database("auth");
|
var gDatabase = new Database("auth");
|
||||||
|
|
||||||
|
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function b64url(value) {
|
||||||
|
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
||||||
|
let equals = value.indexOf('=');
|
||||||
|
if (equals !== -1) {
|
||||||
|
return value.substring(0, equals);
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unb64url(value) {
|
||||||
|
value = value.replaceAll('-', '+').replaceAll('_', '/');
|
||||||
|
let remainder = value.length % 4;
|
||||||
|
if (remainder == 3) {
|
||||||
|
return value + '=';
|
||||||
|
} else if (remainder == 2) {
|
||||||
|
return value + '==';
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeJwt(payload) {
|
||||||
|
let ids = ssb.getIdentities(':auth');
|
||||||
|
let id;
|
||||||
|
if (ids?.length) {
|
||||||
|
id = ids[0];
|
||||||
|
} else {
|
||||||
|
id = ssb.createIdentity(':auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}))));
|
||||||
|
let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.');
|
||||||
|
return jwt;
|
||||||
|
}
|
||||||
|
|
||||||
function readSession(session) {
|
function readSession(session) {
|
||||||
var result = session ? gDatabase.get("session:" + session) : null;
|
let jwt_parts = session?.split('.');
|
||||||
|
if (jwt_parts?.length === 3) {
|
||||||
if (result) {
|
let [header, payload, signature] = jwt_parts;
|
||||||
result = JSON.parse(result);
|
header = JSON.parse(base64Decode(unb64url(header)));
|
||||||
|
if (header.typ === 'JWT' && header.alg === 'HS256') {
|
||||||
let kRefreshInterval = 1 * 60 * 60 * 1000;
|
signature = unb64url(signature);
|
||||||
let now = Date.now();
|
let id = ssb.getIdentities(':auth');
|
||||||
if (!result.lastAccess || result.lastAccess < now - kRefreshInterval) {
|
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
||||||
result.lastAccess = now;
|
let result = JSON.parse(base64Decode(unb64url(payload)));
|
||||||
writeSession(session, result);
|
let now = new Date().valueOf()
|
||||||
|
if (now < result.exp) {
|
||||||
|
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
print(`JWT expired by ${(now - result.exp) / 1000} seconds.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('JWT verification failed.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('Invalid JWT header.');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print('No session JWT.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeSession(session, value) {
|
|
||||||
gDatabase.set("session:" + session, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSession(session, value) {
|
|
||||||
gDatabase.remove("session:" + session);
|
|
||||||
}
|
|
||||||
|
|
||||||
function newSession() {
|
|
||||||
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
var result = "";
|
|
||||||
for (var i = 0; i < 32; i++) {
|
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyPassword(password, hash) {
|
function verifyPassword(password, hash) {
|
||||||
@ -51,25 +81,41 @@ function hashPassword(password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function noAdministrator() {
|
function noAdministrator() {
|
||||||
return !gGlobalSettings || !gGlobalSettings.permissions || !Object.keys(gGlobalSettings.permissions).some(function(name) {
|
return !core.globalSettings || !core.globalSettings.permissions || !Object.keys(core.globalSettings.permissions).some(function(name) {
|
||||||
return gGlobalSettings.permissions[name].indexOf("administration") != -1;
|
return core.globalSettings.permissions[name].indexOf("administration") != -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAdministrator(name) {
|
function makeAdministrator(name) {
|
||||||
if (!gGlobalSettings.permissions) {
|
if (!core.globalSettings.permissions) {
|
||||||
gGlobalSettings.permissions = {};
|
core.globalSettings.permissions = {};
|
||||||
}
|
}
|
||||||
if (!gGlobalSettings.permissions[name]) {
|
if (!core.globalSettings.permissions[name]) {
|
||||||
gGlobalSettings.permissions[name] = [];
|
core.globalSettings.permissions[name] = [];
|
||||||
}
|
}
|
||||||
if (gGlobalSettings.permissions[name].indexOf("administration") == -1) {
|
if (core.globalSettings.permissions[name].indexOf("administration") == -1) {
|
||||||
gGlobalSettings.permissions[name].push("administration");
|
core.globalSettings.permissions[name].push("administration");
|
||||||
}
|
}
|
||||||
setGlobalSettings(gGlobalSettings);
|
core.setGlobalSettings(core.globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function authHandler(request, response) {
|
function getCookies(headers) {
|
||||||
|
var cookies = {};
|
||||||
|
|
||||||
|
if (headers.cookie) {
|
||||||
|
var parts = headers.cookie.split(/,|;/);
|
||||||
|
for (var i in parts) {
|
||||||
|
var equals = parts[i].indexOf("=");
|
||||||
|
var name = parts[i].substring(0, equals).trim();
|
||||||
|
var value = parts[i].substring(equals + 1).trim();
|
||||||
|
cookies[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(request, response) {
|
||||||
var session = getCookies(request.headers).session;
|
var session = getCookies(request.headers).session;
|
||||||
if (request.uri == "/login") {
|
if (request.uri == "/login") {
|
||||||
var sessionIsNew = false;
|
var sessionIsNew = false;
|
||||||
@ -78,16 +124,28 @@ function authHandler(request, response) {
|
|||||||
var formData = form.decodeForm(request.query);
|
var formData = form.decodeForm(request.query);
|
||||||
|
|
||||||
if (request.method == "POST" || formData.submit) {
|
if (request.method == "POST" || formData.submit) {
|
||||||
session = newSession();
|
|
||||||
sessionIsNew = true;
|
sessionIsNew = true;
|
||||||
formData = form.decodeForm(request.body, formData);
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
||||||
if (formData.submit == "Login") {
|
if (formData.submit == "Login") {
|
||||||
var account = gDatabase.get("user:" + formData.name);
|
var account = gDatabase.get("user:" + formData.name);
|
||||||
account = account ? JSON.parse(account) : account;
|
account = account ? JSON.parse(account) : account;
|
||||||
if (formData.register == "1") {
|
if (formData.register == "1") {
|
||||||
if (!account &&
|
if (!account &&
|
||||||
formData.password == formData.confirm) {
|
formData.password == formData.confirm) {
|
||||||
writeSession(session, {name: formData.name});
|
let users = new Set();
|
||||||
|
let users_original = gDatabase.get('users');
|
||||||
|
try {
|
||||||
|
users = new Set(JSON.parse(users_original));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
if (!users.has(formData.name)) {
|
||||||
|
users.add(formData.name);
|
||||||
|
}
|
||||||
|
users = JSON.stringify([...users].sort());
|
||||||
|
if (users !== users_original) {
|
||||||
|
gDatabase.set('users', users);
|
||||||
|
}
|
||||||
|
session = makeJwt({name: formData.name});
|
||||||
account = {password: hashPassword(formData.password)};
|
account = {password: hashPassword(formData.password)};
|
||||||
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
@ -100,7 +158,7 @@ function authHandler(request, response) {
|
|||||||
if (account &&
|
if (account &&
|
||||||
account.password &&
|
account.password &&
|
||||||
verifyPassword(formData.password, account.password)) {
|
verifyPassword(formData.password, account.password)) {
|
||||||
writeSession(session, {name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
makeAdministrator(formData.name);
|
makeAdministrator(formData.name);
|
||||||
}
|
}
|
||||||
@ -110,57 +168,65 @@ function authHandler(request, response) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Proceed as Guest
|
// Proceed as Guest
|
||||||
writeSession(session, {name: "guest"});
|
session = makeJwt({name: 'guest'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cookie = "session=" + session + "; path=/; Max-Age=604800";
|
var cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; Secure; SameSite=Strict`;
|
||||||
var entry = readSession(session);
|
var entry = readSession(session);
|
||||||
if (entry && formData.return) {
|
if (entry && formData.return) {
|
||||||
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
var html = new TextDecoder("UTF-8").decode(File.readFile("core/auth.html"));
|
File.readFile("core/auth.html").then(function(data) {
|
||||||
var contents = "";
|
var html = utf8Decode(data);
|
||||||
|
var contents = "";
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
if (sessionIsNew) {
|
if (sessionIsNew) {
|
||||||
contents += '<div>Welcome back, ' + entry.name + '.</div>\n';
|
contents += '<div>Welcome back, ' + entry.name + '.</div>\n';
|
||||||
|
} else {
|
||||||
|
contents += '<div>You are already logged in, ' + entry.name + '.</div>\n';
|
||||||
|
}
|
||||||
|
contents += '<div><a href="/login/logout">Logout</a></div>\n';
|
||||||
} else {
|
} else {
|
||||||
contents += '<div>You are already logged in, ' + entry.name + '.</div>\n';
|
contents += '<form method="POST">\n';
|
||||||
|
if (loginError) {
|
||||||
|
contents += "<p>" + loginError + "</p>\n";
|
||||||
|
}
|
||||||
|
contents += '<div id="auth_greeting"><b>Halt. Who goes there?</b></div>\n'
|
||||||
|
contents += '<div id="auth">\n';
|
||||||
|
contents += '<div id="auth_login">\n'
|
||||||
|
if (noAdministrator()) {
|
||||||
|
contents += '<div class="notice">There is currently no administrator. You will be made administrator.</div>\n';
|
||||||
|
}
|
||||||
|
contents += '<div><label for="name">Name:</label> <input type="text" id="name" name="name" value=""></div>\n';
|
||||||
|
contents += '<div><label for="password">Password:</label> <input type="password" id="password" name="password" value=""></div>\n';
|
||||||
|
contents += '<div id="confirmPassword" style="display: none"><label for="confirm">Confirm:</label> <input type="password" id="confirm" name="confirm" value=""></div>\n';
|
||||||
|
contents += '<div><input type="checkbox" id="register" name="register" value="1" onchange="showHideConfirm()"> <label for="register">Register a new account</label></div>\n';
|
||||||
|
contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></div>\n';
|
||||||
|
contents += '</div>';
|
||||||
|
contents += '<div class="auth_or"> - or - </div>';
|
||||||
|
contents += '<div id="auth_guest">\n';
|
||||||
|
contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest">\n';
|
||||||
|
contents += '</div>\n';
|
||||||
|
contents += '</div>\n';
|
||||||
|
contents += '<div style="text-align: center">\n';
|
||||||
|
contents += '<h2>Code of Conduct</h2>\n';
|
||||||
|
contents += `<div><textarea readonly rows=20 cols=80>${core.globalSettings.code_of_conduct}</textarea></div>\n`;
|
||||||
|
contents += '</div>\n';
|
||||||
|
contents += '</form>';
|
||||||
}
|
}
|
||||||
contents += '<div><a href="/login/logout">Logout</a></div>\n';
|
var text = html.replace("<!--SESSION-->", contents);
|
||||||
} else {
|
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": text.length});
|
||||||
contents += '<form method="POST">\n';
|
response.end(text);
|
||||||
if (loginError) {
|
}).catch(function(error) {
|
||||||
contents += "<p>" + loginError + "</p>\n";
|
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
}
|
response.end("404 File not found");
|
||||||
contents += '<div id="auth_greeting"><b>Halt. Who goes there?</b></div>\n'
|
});
|
||||||
contents += '<div id="auth">\n';
|
|
||||||
contents += '<div id="auth_login">\n'
|
|
||||||
if (noAdministrator()) {
|
|
||||||
contents += '<div class="notice">There is currently no administrator. You will be made administrator.</div>\n';
|
|
||||||
}
|
|
||||||
contents += '<div><label for="name">Name:</label> <input type="text" id="name" name="name" value=""></div>\n';
|
|
||||||
contents += '<div><label for="password">Password:</label> <input type="password" id="password" name="password" value=""></div>\n';
|
|
||||||
contents += '<div id="confirmPassword" style="display: none"><label for="confirm">Confirm:</label> <input type="password" id="confirm" name="confirm" value=""></div>\n';
|
|
||||||
contents += '<div><input type="checkbox" id="register" name="register" value="1" onchange="showHideConfirm()"> <label for="register">Register a new account</label></div>\n';
|
|
||||||
contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></div>\n';
|
|
||||||
contents += '</div>';
|
|
||||||
contents += '<div class="auth_or"> - or - </div>';
|
|
||||||
contents += '<div id="auth_guest">\n';
|
|
||||||
contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest">\n';
|
|
||||||
contents += '</div>\n';
|
|
||||||
contents += '</div>\n';
|
|
||||||
contents += '</form>';
|
|
||||||
}
|
|
||||||
var text = html.replace("<!--SESSION-->", contents);
|
|
||||||
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": text.length});
|
|
||||||
response.end(text);
|
|
||||||
}
|
}
|
||||||
} else if (request.uri == "/login/logout") {
|
} else if (request.uri == "/login/logout") {
|
||||||
removeSession(session);
|
response.writeHead(303, {"Set-Cookie": "session=; path=/; Secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
||||||
response.writeHead(303, {"Set-Cookie": "session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
@ -180,9 +246,9 @@ function getPermissions(session) {
|
|||||||
|
|
||||||
function getPermissionsForUser(userName) {
|
function getPermissionsForUser(userName) {
|
||||||
var permissions = {};
|
var permissions = {};
|
||||||
if (gGlobalSettings && gGlobalSettings.permissions && gGlobalSettings.permissions[userName]) {
|
if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) {
|
||||||
for (var i in gGlobalSettings.permissions[userName]) {
|
for (var i in core.globalSettings.permissions[userName]) {
|
||||||
permissions[gGlobalSettings.permissions[userName][i]] = true;
|
permissions[core.globalSettings.permissions[userName][i]] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return permissions;
|
return permissions;
|
||||||
@ -191,10 +257,17 @@ function getPermissionsForUser(userName) {
|
|||||||
function query(headers) {
|
function query(headers) {
|
||||||
var session = getCookies(headers).session;
|
var session = getCookies(headers).session;
|
||||||
var entry;
|
var entry;
|
||||||
if (entry = readSession(session)) {
|
var autologin = tildefriends.args.autologin;
|
||||||
return {session: entry, permissions: getPermissions(session)};
|
if (entry = autologin ? {name: autologin} : readSession(session)) {
|
||||||
|
return {
|
||||||
|
session: entry,
|
||||||
|
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
||||||
|
refresh: {
|
||||||
|
token: makeJwt({name: entry.name}),
|
||||||
|
interval: kRefreshInterval,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.handler = authHandler;
|
export { handler, query };
|
||||||
exports.query = query;
|
|
||||||
|
929
core/client.js
929
core/client.js
File diff suppressed because it is too large
Load Diff
865
core/core.js
865
core/core.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
3309
core/encoding.js
3309
core/encoding.js
File diff suppressed because it is too large
Load Diff
@ -30,4 +30,4 @@ function decodeForm(encoded, initial) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.decodeForm = decodeForm;
|
export { decodeForm };
|
||||||
|
@ -31,7 +31,7 @@ function parseResponse(data) {
|
|||||||
return {body: data};
|
return {body: data};
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(url) {
|
export function get(url) {
|
||||||
var parsed = parseUrl(url);
|
var parsed = parseUrl(url);
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var socket = new Socket();
|
var socket = new Socket();
|
||||||
@ -57,5 +57,3 @@ function get(url) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.get = get;
|
|
||||||
|
238
core/httpd.js
238
core/httpd.js
@ -1,10 +1,19 @@
|
|||||||
|
import * as sha1 from './sha1.js';
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var gHandlers = [];
|
var gHandlers = [];
|
||||||
var gSocketHandlers = [];
|
var gSocketHandlers = [];
|
||||||
|
var gBadRequests = {};
|
||||||
|
|
||||||
|
const kRequestTimeout = 15000;
|
||||||
|
const kStallTimeout = 60000;
|
||||||
|
|
||||||
function logError(error) {
|
function logError(error) {
|
||||||
print("ERROR " + error);
|
print("ERROR " + error);
|
||||||
|
if (error.stackTrace) {
|
||||||
|
print(error.stackTrace);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addHandler(handler) {
|
function addHandler(handler) {
|
||||||
@ -48,9 +57,9 @@ function Request(method, uri, version, headers, body, client) {
|
|||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.query = undefined;
|
this.query = undefined;
|
||||||
}
|
}
|
||||||
this.version = version;
|
this.version = version || '';
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
this.client = {peerName: client.peerName};
|
this.client = {peerName: client.peerName, tls: client.tls};
|
||||||
this.body = body;
|
this.body = body;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -84,6 +93,8 @@ function Response(request, client) {
|
|||||||
101: "Switching Protocols",
|
101: "Switching Protocols",
|
||||||
200: 'OK',
|
200: 'OK',
|
||||||
303: 'See other',
|
303: 'See other',
|
||||||
|
304: 'Not Modified',
|
||||||
|
400: 'Bad Request',
|
||||||
403: 'Forbidden',
|
403: 'Forbidden',
|
||||||
404: 'File not found',
|
404: 'File not found',
|
||||||
500: 'Internal server error',
|
500: 'Internal server error',
|
||||||
@ -127,7 +138,7 @@ function Response(request, client) {
|
|||||||
}
|
}
|
||||||
headerString += "\r\n";
|
headerString += "\r\n";
|
||||||
_started = true;
|
_started = true;
|
||||||
client.write(headerString);
|
client.write(headerString).catch(function() {});
|
||||||
},
|
},
|
||||||
end: function(data) {
|
end: function(data) {
|
||||||
if (_finished) {
|
if (_finished) {
|
||||||
@ -135,25 +146,24 @@ function Response(request, client) {
|
|||||||
}
|
}
|
||||||
if (data) {
|
if (data) {
|
||||||
if (_chunked) {
|
if (_chunked) {
|
||||||
client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n");
|
client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n").catch(function() {});
|
||||||
} else {
|
} else {
|
||||||
client.write(data);
|
client.write(data).catch(function() {});
|
||||||
}
|
}
|
||||||
} else if (_chunked) {
|
} else if (_chunked) {
|
||||||
client.write("0\r\n\r\n");
|
client.write("0\r\n\r\n").catch(function() {});
|
||||||
}
|
}
|
||||||
_finished = true;
|
_finished = true;
|
||||||
if (!_keepAlive) {
|
if (!_keepAlive) {
|
||||||
client.shutdown();
|
client.shutdown().catch(function() {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
reportError: function(error) {
|
reportError: function(error) {
|
||||||
if (!_started) {
|
if (!_started) {
|
||||||
client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n");
|
client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n").catch(function() {});
|
||||||
}
|
}
|
||||||
if (!_finished) {
|
if (!_finished) {
|
||||||
client.write("500 Internal Server Error\r\n\r\n" + error.stackTrace);
|
client.write("500 Internal Server Error\r\n\r\n" + error?.stackTrace).catch(function() {});
|
||||||
client.shutdown();
|
|
||||||
}
|
}
|
||||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||||
},
|
},
|
||||||
@ -172,11 +182,12 @@ function handleRequest(request, response) {
|
|||||||
if (promise) {
|
if (promise) {
|
||||||
promise.catch(function(error) {
|
promise.catch(function(error) {
|
||||||
response.reportError(error);
|
response.reportError(error);
|
||||||
|
request.client.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
print(error);
|
|
||||||
response.reportError(error);
|
response.reportError(error);
|
||||||
|
request.client.close();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
|
||||||
@ -200,7 +211,7 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
opCode = 0x2;
|
opCode = 0x2;
|
||||||
}
|
}
|
||||||
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
|
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
|
||||||
message = new TextEncoder("UTF-8").encode(message);
|
message = utf8Encode(message);
|
||||||
}
|
}
|
||||||
var fin = true;
|
var fin = true;
|
||||||
var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
||||||
@ -228,11 +239,16 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
var array = new Uint8Array(packet.length + message.length);
|
var array = new Uint8Array(packet.length + message.length);
|
||||||
array.set(packet, 0);
|
array.set(packet, 0);
|
||||||
array.set(message, packet.length);
|
array.set(message, packet.length);
|
||||||
return client.write(array);
|
try {
|
||||||
|
return client.write(array);
|
||||||
|
} catch (error) {
|
||||||
|
client.close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
response.onMessage = null;
|
response.onMessage = null;
|
||||||
|
|
||||||
handler.invoke(request, response);
|
let extra_headers = handler.invoke(request, response);
|
||||||
|
|
||||||
client.read(function(data) {
|
client.read(function(data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -291,7 +307,7 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
if (fin) {
|
if (fin) {
|
||||||
if (response.onMessage) {
|
if (response.onMessage) {
|
||||||
response.onMessage({
|
response.onMessage({
|
||||||
data: frameOpCode == 0x1 ? new TextDecoder("UTF-8").decode(frame) : frame,
|
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
|
||||||
opCode: frameOpCode,
|
opCode: frameOpCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -301,10 +317,14 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
response.onClose();
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.onError(function(error) {
|
client.onError(function(error) {
|
||||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||||
|
response.onError(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
let headers = {
|
let headers = {
|
||||||
@ -315,13 +335,13 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
if (request.headers["sec-websocket-version"] != "13") {
|
if (request.headers["sec-websocket-version"] != "13") {
|
||||||
headers["Sec-WebSocket-Version"] = "13";
|
headers["Sec-WebSocket-Version"] = "13";
|
||||||
}
|
}
|
||||||
response.writeHead(101, headers);
|
response.writeHead(101, Object.assign({}, headers, extra_headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
function webSocketAcceptResponse(key) {
|
function webSocketAcceptResponse(key) {
|
||||||
var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||||
var hex = require("sha1").hash(key + kMagic)
|
var hex = sha1.hash(key + kMagic)
|
||||||
var binary = "";
|
var binary = "";
|
||||||
for (var i = 0; i < hex.length; i += 6) {
|
for (var i = 0; i < hex.length; i += 6) {
|
||||||
var characters = hex.substring(i, i + 6);
|
var characters = hex.substring(i, i + 6);
|
||||||
@ -340,13 +360,91 @@ function webSocketAcceptResponse(key) {
|
|||||||
return binary;
|
return binary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function badRequest(client, reason) {
|
||||||
|
var now = new Date();
|
||||||
|
var count = 0;
|
||||||
|
var old = gBadRequests[client.peerName];
|
||||||
|
if (!old) {
|
||||||
|
gBadRequests[client.peerName] = {
|
||||||
|
expire: new Date(now.getTime() + 1 * 60 * 1000),
|
||||||
|
count: 1,
|
||||||
|
reason: reason,
|
||||||
|
};
|
||||||
|
count = 1;
|
||||||
|
} else {
|
||||||
|
old.count++;
|
||||||
|
old.reason = reason;
|
||||||
|
count = old.count;
|
||||||
|
}
|
||||||
|
new Response({version: '1.0'}, client).reportError(reason + ': ' + count);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function allowRequest(client) {
|
||||||
|
var old = gBadRequests[client.peerName];
|
||||||
|
if (old) {
|
||||||
|
var now = new Date();
|
||||||
|
if (old.expire < now) {
|
||||||
|
delete gBadRequests[client.peerName];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return old.count < 3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleConnection(client) {
|
function handleConnection(client) {
|
||||||
|
if (!allowRequest(client)) {
|
||||||
|
print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason);
|
||||||
|
client.info = 'rejected';
|
||||||
|
client.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.info = 'accepted';
|
||||||
var inputBuffer = new Uint8Array(0);
|
var inputBuffer = new Uint8Array(0);
|
||||||
var request;
|
var request;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
var lineByLine = true;
|
var lineByLine = true;
|
||||||
var bodyToRead = -1;
|
var bodyToRead = -1;
|
||||||
var body;
|
var body;
|
||||||
|
var requestCount = -1;
|
||||||
|
var readCount = 0;
|
||||||
|
var isWebsocket = false;
|
||||||
|
|
||||||
|
function resetTimeout(requestIndex) {
|
||||||
|
if (isWebsocket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bodyToRead == -1) {
|
||||||
|
setTimeout(function() {
|
||||||
|
if (requestCount == requestIndex) {
|
||||||
|
client.info = 'timed out';
|
||||||
|
if (requestCount == 0) {
|
||||||
|
badRequest(client, 'Timed out waiting for request.');
|
||||||
|
} else {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, kRequestTimeout);
|
||||||
|
} else {
|
||||||
|
var lastReadCount = readCount;
|
||||||
|
setTimeout(function() {
|
||||||
|
if (readCount == lastReadCount) {
|
||||||
|
client.info = 'stalled';
|
||||||
|
if (requestCount == 0) {
|
||||||
|
badRequest(client, 'Request stalled.');
|
||||||
|
} else {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, kStallTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTimeout(++requestCount);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
inputBuffer = new Uint8Array(0);
|
inputBuffer = new Uint8Array(0);
|
||||||
@ -355,9 +453,12 @@ function handleConnection(client) {
|
|||||||
lineByLine = true;
|
lineByLine = true;
|
||||||
bodyToRead = -1;
|
bodyToRead = -1;
|
||||||
body = undefined;
|
body = undefined;
|
||||||
|
client.info = 'reset';
|
||||||
|
resetTimeout(++requestCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
|
client.info = 'finishing';
|
||||||
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
var response = new Response(requestObject, client);
|
var response = new Response(requestObject, client);
|
||||||
try {
|
try {
|
||||||
@ -367,14 +468,24 @@ function handleConnection(client) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
response.reportError(error);
|
response.reportError(error);
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLine(line, length) {
|
function handleLine(line, length) {
|
||||||
if (bodyToRead == -1) {
|
if (bodyToRead == -1) {
|
||||||
line = new TextDecoder("ASCII").decode(line);
|
line = utf8Decode(line);
|
||||||
if (!request) {
|
if (!request) {
|
||||||
|
if (!line) {
|
||||||
|
badRequest(client, 'Empty request.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
request = line.split(' ');
|
request = line.split(' ');
|
||||||
|
if (request.length != 3 || !request[2].startsWith('HTTP/1.')) {
|
||||||
|
badRequest(client, 'Bad request.');
|
||||||
|
request = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (line) {
|
} else if (line) {
|
||||||
var colon = line.indexOf(':');
|
var colon = line.indexOf(':');
|
||||||
@ -386,15 +497,25 @@ function handleConnection(client) {
|
|||||||
if (headers["content-length"] != undefined) {
|
if (headers["content-length"] != undefined) {
|
||||||
bodyToRead = parseInt(headers["content-length"]);
|
bodyToRead = parseInt(headers["content-length"]);
|
||||||
lineByLine = false;
|
lineByLine = false;
|
||||||
body = "";
|
if (bodyToRead > 16 * 1024 * 1024) {
|
||||||
|
badRequest(client, 'Request too large: ' + bodyToRead + '.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
body = new Uint8Array(bodyToRead);
|
||||||
|
client.info = 'waiting for body';
|
||||||
|
resetTimeout(requestCount);
|
||||||
return true;
|
return true;
|
||||||
} else if (headers["connection"]
|
} else if (headers["connection"]
|
||||||
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
||||||
&& headers["upgrade"]
|
&& headers["upgrade"]
|
||||||
&& headers["upgrade"].toLowerCase() == "websocket") {
|
&& headers["upgrade"].toLowerCase() == "websocket") {
|
||||||
|
isWebsocket = true;
|
||||||
|
client.info = 'websocket';
|
||||||
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
var response = new Response(requestObject, client);
|
var response = new Response(requestObject, client);
|
||||||
handleWebSocketRequest(requestObject, response, client);
|
handleWebSocketRequest(requestObject, response, client);
|
||||||
|
/* Prevent the timeout from disconnecting us. */
|
||||||
|
requestCount++;
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
finish();
|
finish();
|
||||||
@ -402,9 +523,12 @@ function handleConnection(client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
line = new TextDecoder("UTF-8").decode(line);
|
var offset = body.length - bodyToRead;
|
||||||
body += line;
|
if (line.length > body.length - offset) {
|
||||||
bodyToRead -= length;
|
line = line.slice(0, body.length - offset);
|
||||||
|
}
|
||||||
|
body.set(line, offset);
|
||||||
|
bodyToRead -= line.length;
|
||||||
if (bodyToRead <= 0) {
|
if (bodyToRead <= 0) {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@ -418,7 +542,12 @@ function handleConnection(client) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.read(function(data) {
|
client.read(function(data) {
|
||||||
|
readCount++;
|
||||||
if (data) {
|
if (data) {
|
||||||
|
if (bodyToRead != -1 && !isWebsocket) {
|
||||||
|
resetTimeout(requestCount);
|
||||||
|
}
|
||||||
|
const kMaxLineLength = 4096;
|
||||||
var newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
var newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
||||||
newBuffer.set(inputBuffer, 0);
|
newBuffer.set(inputBuffer, 0);
|
||||||
newBuffer.set(data, inputBuffer.length);
|
newBuffer.set(data, inputBuffer.length);
|
||||||
@ -436,6 +565,10 @@ function handleConnection(client) {
|
|||||||
if (end > 0 && inputBuffer[end - 1] == carriageReturn) {
|
if (end > 0 && inputBuffer[end - 1] == carriageReturn) {
|
||||||
--end;
|
--end;
|
||||||
}
|
}
|
||||||
|
if (end > kMaxLineLength || end == -1 && inputBuffer.length > kMaxLineLength) {
|
||||||
|
badRequest(client, 'Request too long.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (end != -1) {
|
if (end != -1) {
|
||||||
var line = inputBuffer.slice(0, end);
|
var line = inputBuffer.slice(0, end);
|
||||||
inputBuffer = inputBuffer.slice(realEnd + 1);
|
inputBuffer = inputBuffer.slice(realEnd + 1);
|
||||||
@ -446,6 +579,9 @@ function handleConnection(client) {
|
|||||||
inputBuffer = new Uint8Array(0);
|
inputBuffer = new Uint8Array(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
client.info = 'EOF';
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -468,43 +604,43 @@ if (tildefriends.https_port) {
|
|||||||
var tls = {};
|
var tls = {};
|
||||||
var secureSocket = new Socket();
|
var secureSocket = new Socket();
|
||||||
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
|
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
|
||||||
return secureSocket.listen(kBacklog, function() {
|
return secureSocket.listen(kBacklog, async function() {
|
||||||
return secureSocket.accept().then(function(client) {
|
try {
|
||||||
handleConnection(client);
|
var client = await secureSocket.accept();
|
||||||
|
client.tls = true;
|
||||||
const kCertificatePath = "data/httpd/certificate.pem";
|
const kCertificatePath = "data/httpd/certificate.pem";
|
||||||
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
||||||
|
|
||||||
return Promise.all([
|
var stat = await Promise.all([
|
||||||
File.stat(kCertificatePath),
|
await File.stat(kCertificatePath),
|
||||||
File.stat(kPrivateKeyPath),
|
await File.stat(kPrivateKeyPath),
|
||||||
]).then(function(stat) {
|
]);
|
||||||
if (!tls.context ||
|
if (!tls.context ||
|
||||||
tls.certStat.mtime != stat[0].mtime ||
|
tls.certStat.mtime != stat[0].mtime ||
|
||||||
tls.certStat.size != stat[0].size ||
|
tls.certStat.size != stat[0].size ||
|
||||||
tls.keyStat.mtime != stat[1].mtime ||
|
tls.keyStat.mtime != stat[1].mtime ||
|
||||||
tls.keyStat.size != stat[1].size) {
|
tls.keyStat.size != stat[1].size) {
|
||||||
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
|
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
|
||||||
var privateKey = new TextDecoder("ASCII").decode(File.readFile(kPrivateKeyPath));
|
var privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||||
var certificate = new TextDecoder("ASCII").decode(File.readFile(kCertificatePath));
|
var certificate = utf8Decode(await File.readFile(kCertificatePath));
|
||||||
|
|
||||||
tls.context = new TlsContext();
|
tls.context = new TlsContext();
|
||||||
tls.context.setPrivateKey(privateKey);
|
tls.context.setPrivateKey(privateKey);
|
||||||
tls.context.setCertificate(certificate);
|
tls.context.setCertificate(certificate);
|
||||||
tls.certStat = stat[0];
|
tls.certStat = stat[0];
|
||||||
tls.keyStat = stat[1];
|
tls.keyStat = stat[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.startTls(tls.context);
|
let result = client.startTls(tls.context);
|
||||||
}).catch(function(error) {
|
handleConnection(client);
|
||||||
logError("[" + new Date() + "] [" + client.peerName + "] " + error);
|
return result;
|
||||||
});
|
} catch (error) {
|
||||||
});
|
logError("[" + new Date() + "] [" + client.peerName + "] " + error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
logError("[" + new Date() + "] bind error " + error);
|
logError("[" + new Date() + "] bind error " + error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.all = all;
|
export { all, registerSocketHandler };
|
||||||
exports.registerSocketHandler = registerSocketHandler;
|
|
||||||
|
@ -5,35 +5,52 @@
|
|||||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
||||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<!--HEAD-->
|
|
||||||
</head>
|
</head>
|
||||||
<body style="display: flex; flex-flow: column">
|
<body style="display: flex; flex-flow: column">
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
<span>😎</span>
|
<span>😎</span>
|
||||||
<span id="title">Tilde Friends</span>
|
<a accesskey="h" data-tip="Open home app." href="/" style="color: #fff">Tilde Friends</a>
|
||||||
<a href="/">home</a>
|
<a accesskey="a" data-tip="Open apps list." href="/~core/apps/">apps</a>
|
||||||
<a href="#" onclick="event.preventDefault(); edit()">edit</a>
|
<a accesskey="e" data-tip="Toggle the app editor." href="#" onclick="event.preventDefault(); toggleEdit()">edit</a>
|
||||||
<a href="/trace">trace</a>
|
<a accesskey="p" data-tip="View and change permissions." href="#" onclick="showPermissions()">🎛️</a>
|
||||||
<span id="status"></span>
|
<span id="status"></span>
|
||||||
|
<span id="requests"></span>
|
||||||
|
<span id="permissions_settings"></span>
|
||||||
|
<span id="permissions"></span>
|
||||||
<span id="login"></span>
|
<span id="login"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
|
<div id="content" class="hbox" style="flex: 1 1; width: 100%">
|
||||||
|
<div id="statsPane" class="vbox" style="display: none; flex 1 1">
|
||||||
|
<div class="hbox">
|
||||||
|
<input type="button" id="closeStats" name="closeStats" value="Close" onclick="closeStats()">
|
||||||
|
</div>
|
||||||
|
<div id="graphs" class="vbox" style="height: 100%"></div>
|
||||||
|
</div>
|
||||||
<div id="editPane" class="vbox" style="display: none">
|
<div id="editPane" class="vbox" style="display: none">
|
||||||
<div class="navigation">
|
<div class="navigation hbox">
|
||||||
<input type="button" id="closeEditor" name="closeEditor" value="Close" onclick="closeEditor()">
|
<input type="button" id="closeEditor" name="closeEditor" value="Close" onclick="closeEditor()">
|
||||||
<input type="button" id="save" name="save" value="Save" onclick="save()">
|
<input type="button" id="save" name="save" value="Save" onclick="save()">
|
||||||
<input type="text" id="name" name="name"></input>
|
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
||||||
<input type="checkbox" id="run" name="run" checked><label for="run">Restart after save</label>
|
<input type="button" id="push_to_parent" value="Push to Parent" onclick="pushToParent()">
|
||||||
<input type="button" id="revert" name="revert" value="Revert to Saved" onclick="revert()">
|
<input type="button" id="pull_from_parent" value="Pull from Parent" onclick="pullFromParent()">
|
||||||
<a id="latest" href="">Latest</a>
|
<input type="button" id="revert" name="revert" value="Revert" onclick="revert()">
|
||||||
|
<input type="button" id="delete" name="delete" value="Delete" onclick="deleteApp()">
|
||||||
|
<input type="button" onclick="event.preventDefault(); trace()" value="Trace">
|
||||||
|
<input type="button" onclick="event.preventDefault(); toggleStats()" value="Stats">
|
||||||
</div>
|
</div>
|
||||||
<div class="hbox" style="height: 100%">
|
<div class="hbox" style="height: 100%">
|
||||||
<div id="filesPane">
|
<div id="filesPane">
|
||||||
<ul id="files">
|
<div class="hbox">
|
||||||
</ul>
|
<span id="files_header">Files</span>
|
||||||
<br>
|
<span id="files_hide" onclick="hideFiles()">«</span>
|
||||||
<div><button onclick="newFile()">New File</button></div>
|
<span id="files_show" onclick="showFiles()">»</span>
|
||||||
<div><button onclick="removeFile()">Remove File</button></div>
|
</div>
|
||||||
|
<div id="files_content">
|
||||||
|
<ul id="files"></ul>
|
||||||
|
<br>
|
||||||
|
<div><button onclick="newFile()">New File</button></div>
|
||||||
|
<div><button onclick="removeFile()">Remove File</button></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
|
<div id="docPane" style="display: flex; flex: 1 1 50%; flex-flow: column">
|
||||||
<div style="flex: 1 1 50%; position: relative">
|
<div style="flex: 1 1 50%; position: relative">
|
||||||
@ -42,10 +59,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vbox" style="flex: 1 0 50%; overflow: auto">
|
<div id="viewPane" class="vbox" style="flex: 1 0; overflow: auto">
|
||||||
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals" style="width: 100%; height: 100%; border: 0"></iframe>
|
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="/split/split.min.js"></script>
|
||||||
|
<script src="/smoothie/smoothie.js"></script>
|
||||||
<script src="/static/client.js"></script>
|
<script src="/static/client.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -157,4 +157,4 @@ if (typeof String.prototype.utf8Decode == 'undefined') {
|
|||||||
if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export
|
if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export
|
||||||
if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD
|
if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD
|
||||||
|
|
||||||
exports.hash = Sha1.hash;
|
export let hash = Sha1.hash;
|
||||||
|
370
core/ssb.js
Normal file
370
core/ssb.js
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
"use strict";
|
||||||
|
var g_wants_requests = {};
|
||||||
|
var g_database = new Database('core');
|
||||||
|
let g_attendants = {};
|
||||||
|
const k_use_create_history_stream = false;
|
||||||
|
const k_blobs_concurrent_target = 8;
|
||||||
|
const k_settings = JSON.parse(g_database.get('settings') ?? '{"room": true}');
|
||||||
|
|
||||||
|
function following(db, id) {
|
||||||
|
var o = db.get(id + ":following");
|
||||||
|
const k_version = 5;
|
||||||
|
var f = o ? JSON.parse(o) : o;
|
||||||
|
if (!f || f.version != k_version) {
|
||||||
|
f = {users: [], sequence: 0, version: k_version};
|
||||||
|
}
|
||||||
|
f.users = new Set(f.users);
|
||||||
|
ssb.sqlStream(
|
||||||
|
"SELECT "+
|
||||||
|
" sequence, "+
|
||||||
|
" json_extract(content, '$.contact') AS contact, "+
|
||||||
|
" json_extract(content, '$.following') AS following "+
|
||||||
|
"FROM messages "+
|
||||||
|
"WHERE "+
|
||||||
|
" author = ?1 AND "+
|
||||||
|
" sequence > ?2 AND "+
|
||||||
|
" json_extract(content, '$.type') = 'contact' "+
|
||||||
|
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
||||||
|
"ORDER BY sequence",
|
||||||
|
[id, f.sequence],
|
||||||
|
function(row) {
|
||||||
|
if (row.following) {
|
||||||
|
f.users.add(row.contact);
|
||||||
|
} else {
|
||||||
|
f.users.delete(row.contact);
|
||||||
|
}
|
||||||
|
f.sequence = row.sequence;
|
||||||
|
});
|
||||||
|
f.users = Array.from(f.users).sort();
|
||||||
|
var j = JSON.stringify(f);
|
||||||
|
if (o != j) {
|
||||||
|
db.set(id + ":following", j);
|
||||||
|
}
|
||||||
|
return f.users;
|
||||||
|
}
|
||||||
|
|
||||||
|
function followingDeep(db, seed_ids, depth) {
|
||||||
|
if (depth <= 0) {
|
||||||
|
return seed_ids;
|
||||||
|
}
|
||||||
|
var f = seed_ids.map(x => following(db, x));
|
||||||
|
var ids = [].concat(...f);
|
||||||
|
var x = followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
||||||
|
x = [...new Set([].concat(...x, ...seed_ids))].sort();
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_latest_sequence_for_author(author) {
|
||||||
|
var sequence = 0;
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT MAX(sequence) AS sequence FROM messages WHERE author = ?1',
|
||||||
|
[author],
|
||||||
|
function(row) {
|
||||||
|
if (row.sequence) {
|
||||||
|
sequence = row.sequence;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeMessage(message) {
|
||||||
|
var payload = message.message.value ? message.message.value : message.message;
|
||||||
|
if (typeof(payload) == 'object') {
|
||||||
|
ssb.storeMessage(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tunnel_attendants(request) {
|
||||||
|
if (request.message.type !== 'state') {
|
||||||
|
throw Error('Unexpected type: ' + request.message.type);
|
||||||
|
}
|
||||||
|
let state = new Set(request.message.ids);
|
||||||
|
for (let id of state) {
|
||||||
|
request.add_room_attendant(id);
|
||||||
|
}
|
||||||
|
request.more(function attendants(message) {
|
||||||
|
if (message.message.type === 'joined') {
|
||||||
|
request.add_room_attendant(message.message.id);
|
||||||
|
state.add(message.message.id);
|
||||||
|
} else if (message.message.type === 'left') {
|
||||||
|
request.remove_room_attendant(message.message.id);
|
||||||
|
state.delete(message.message.id);
|
||||||
|
} else {
|
||||||
|
throw Error('Unexpected type: ' + message.type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_blobs_create_wants(connection) {
|
||||||
|
connection.send_json({'name': ['blobs', 'createWants'], 'type': 'source', 'args': []}, function on_blob_create_wants(message) {
|
||||||
|
if (message.message?.name === 'Error') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.keys(message.message).forEach(function(id) {
|
||||||
|
if (message.message[id] < 0) {
|
||||||
|
if (g_wants_requests[connection.id]) {
|
||||||
|
delete connection.active_blob_wants[id];
|
||||||
|
var blob = ssb.blobGet(id);
|
||||||
|
if (blob) {
|
||||||
|
var out_message = {};
|
||||||
|
out_message[id] = blob.byteLength;
|
||||||
|
g_wants_requests[connection.id].send_json(out_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var received_bytes = 0;
|
||||||
|
var expected_bytes = message.message[id];
|
||||||
|
var buffer = new Uint8Array(expected_bytes);
|
||||||
|
connection.send_json({'name': ['blobs', 'get'], 'type': 'source', 'args': [id]}, function(message) {
|
||||||
|
if (message.flags & 0x4 /* end */) {
|
||||||
|
delete connection.active_blob_wants[id];
|
||||||
|
} else {
|
||||||
|
buffer.set(new Uint8Array(message.message, 0, message.message.byteLength), received_bytes);
|
||||||
|
received_bytes += message.message.byteLength;
|
||||||
|
if (received_bytes == expected_bytes) {
|
||||||
|
ssb.blobStore(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (g_wants_requests[connection.id] && Object.keys(connection.active_blob_wants).length < k_blobs_concurrent_target) {
|
||||||
|
requestMoreBlobs(g_wants_requests[connection.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addEventListener('connections', function on_connections_changed(change, connection) {
|
||||||
|
if (change == 'add') {
|
||||||
|
connection.active_blob_wants = {};
|
||||||
|
var sequence = get_latest_sequence_for_author(connection.id);
|
||||||
|
if (k_use_create_history_stream) {
|
||||||
|
connection.send_json({'name': ['createHistoryStream'], 'type': 'source', 'args': [{'id': connection.id, 'seq': sequence, 'live': true, 'keys': false}]}, storeMessage);
|
||||||
|
var identities = ssb.getAllIdentities();
|
||||||
|
followingDeep(g_database, identities, 2).then(function(ids) {
|
||||||
|
for (let id of ids) {
|
||||||
|
if (identities.indexOf(id) != -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var sequence = get_latest_sequence_for_author(id);
|
||||||
|
connection.send_json({'name': ['createHistoryStream'], 'type': 'source', 'args': [{'id': id, 'seq': sequence, 'live': true, 'keys': false}]}, storeMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (connection.is_client) {
|
||||||
|
connection.send_json({"name": ["ebt", "replicate"], "args": [{"version": 3, "format": "classic"}], "type": "duplex"}, ebtReplicateClient);
|
||||||
|
|
||||||
|
connection.send_json_async({'name': ['tunnel', 'isRoom'], 'args': []}, function tunnel_is_room(request) {
|
||||||
|
if (request.message) {
|
||||||
|
request.connection.send_json({'name': ['room', 'attendants'], 'args': [], 'type': 'source'}, tunnel_attendants);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
send_blobs_create_wants(connection);
|
||||||
|
}
|
||||||
|
} else if (change == 'remove') {
|
||||||
|
print('REMOVE', connection.id);
|
||||||
|
notify_attendant_changed(connection.id, 'left');
|
||||||
|
delete g_attendants[connection.id];
|
||||||
|
delete g_wants_requests[connection.id];
|
||||||
|
} else {
|
||||||
|
print('CHANGE', change);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function blob_want_discovered(request, id) {
|
||||||
|
if (!request || !request.connection || Object.keys(request.connection.active_blob_wants).length > k_blobs_concurrent_target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var message = {};
|
||||||
|
message[id] = -1;
|
||||||
|
request.send_json(message);
|
||||||
|
request.connection.active_blob_wants[id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMoreBlobs(request) {
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT id FROM blob_wants LIMIT ' + k_blobs_concurrent_target,
|
||||||
|
[],
|
||||||
|
row => blob_want_discovered(request, row.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addRpc(['blobs', 'createWants'], function(request) {
|
||||||
|
g_wants_requests[request.connection.id] = request;
|
||||||
|
ssb.addEventListener('blob_want_added', id => blob_want_discovered(request, id));
|
||||||
|
requestMoreBlobs(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
ssb.addRpc(['tunnel', 'isRoom'], function(request) {
|
||||||
|
if (k_settings.room) {
|
||||||
|
request.send_json({"name": "tilde friends tunnel", "membership": false, "features": ["tunnel", "room1"]});
|
||||||
|
} else {
|
||||||
|
request.send_json(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function notify_attendant_changed(id, type) {
|
||||||
|
if (!id) {
|
||||||
|
print(`notify_attendant_changed called with id=${id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let r of Object.values(g_attendants)) {
|
||||||
|
try {
|
||||||
|
r.send_json({
|
||||||
|
type: type,
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print(`Removing ${id} from g_attendants in ${type}.`, e);
|
||||||
|
delete g_attendants[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addRpc(['room', 'attendants'], function(request) {
|
||||||
|
let ids = Object.keys(g_attendants).sort();
|
||||||
|
request.send_json({
|
||||||
|
type: 'state',
|
||||||
|
ids: ids,
|
||||||
|
});
|
||||||
|
notify_attendant_changed(request.connection.id, 'joined');
|
||||||
|
g_attendants[request.connection.id] = request;
|
||||||
|
});
|
||||||
|
|
||||||
|
function ebtReplicateSendClock(request, have) {
|
||||||
|
var identities = ssb.getAllIdentities();
|
||||||
|
var message = {};
|
||||||
|
var last_sent = request.connection.sent_clock || {};
|
||||||
|
var ids = followingDeep(g_database, identities, 2).concat([request.connection.id]);
|
||||||
|
if (!Object.keys(last_sent).length) {
|
||||||
|
for (let id of ids) {
|
||||||
|
message[id] = get_latest_sequence_for_author(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let id of Object.keys(have)) {
|
||||||
|
if (message[id] === undefined) {
|
||||||
|
var sequence = get_latest_sequence_for_author(id);
|
||||||
|
message[id] = sequence ? sequence : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var to_send = {}
|
||||||
|
var offset = Math.floor(Math.random() * ids.length);
|
||||||
|
for (var i = 0; i < ids.length; i++) {
|
||||||
|
var id = ids[(i + offset) % ids.length];
|
||||||
|
if (last_sent[id] === undefined || message[id] > last_sent[id]) {
|
||||||
|
last_sent[id] = to_send[id] = message[id] === -1 ? -1 : message[id] << 1;
|
||||||
|
}
|
||||||
|
if (Object.keys(to_send).length >= 32) {
|
||||||
|
request.send_json(to_send);
|
||||||
|
to_send = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.connection.sent_clock = last_sent;
|
||||||
|
|
||||||
|
if (Object.keys(to_send).length) {
|
||||||
|
request.send_json(to_send);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessage(row) {
|
||||||
|
if (row.sequence_before_author) {
|
||||||
|
return {
|
||||||
|
previous: row.previous,
|
||||||
|
sequence: row.sequence,
|
||||||
|
author: row.author,
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
hash: row.hash,
|
||||||
|
content: JSON.parse(row.content),
|
||||||
|
signature: row.signature,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
previous: row.previous,
|
||||||
|
author: row.author,
|
||||||
|
sequence: row.sequence,
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
hash: row.hash,
|
||||||
|
content: JSON.parse(row.content),
|
||||||
|
signature: row.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ebtReplicateRegisterMessageCallback(request) {
|
||||||
|
ssb.addEventListener('message', function(message_id) {
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT previous, author, id, sequence, timestamp, hash, content, signature FROM messages WHERE id = ?1',
|
||||||
|
[message_id],
|
||||||
|
function (row) {
|
||||||
|
request.send_json(formatMessage(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ebtReplicateCommon(request) {
|
||||||
|
if (request.message.author) {
|
||||||
|
storeMessage(request);
|
||||||
|
} else {
|
||||||
|
ebtReplicateSendClock(request, request.message);
|
||||||
|
|
||||||
|
for (let id of Object.keys(request.message)) {
|
||||||
|
if (request.message[id] >= 0 && (request.message[id] & 1) == 0) {
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT previous, author, id, sequence, timestamp, hash, content, signature FROM messages WHERE author = ?1 AND sequence >= ?2 ORDER BY sequence',
|
||||||
|
[id, request.message[id] >> 1],
|
||||||
|
function (row) {
|
||||||
|
request.send_json(formatMessage(row));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ebtReplicateClient(request) {
|
||||||
|
if (request.message?.name !== 'Error') {
|
||||||
|
if (!request.connection.message_registered) {
|
||||||
|
ebtReplicateRegisterMessageCallback(request);
|
||||||
|
request.connection.message_registered = true;
|
||||||
|
}
|
||||||
|
ebtReplicateCommon(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ebtReplicateServer(request) {
|
||||||
|
ebtReplicateRegisterMessageCallback(request);
|
||||||
|
ebtReplicateSendClock(request, {});
|
||||||
|
request.more(ebtReplicateCommon);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addRpc(['ebt', 'replicate'], ebtReplicateServer);
|
||||||
|
|
||||||
|
ssb.addRpc(['createHistoryStream'], function(request) {
|
||||||
|
var id = request.args[0].id;
|
||||||
|
var seq = request.args[0].seq;
|
||||||
|
var keys = request.args[0].keys || request.args[0].keys === undefined;
|
||||||
|
function sendMessage(row) {
|
||||||
|
if (keys) {
|
||||||
|
var message = {
|
||||||
|
key: row.id,
|
||||||
|
value: formatMessage(row),
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
var message = formatMessage(row);
|
||||||
|
}
|
||||||
|
request.send_json(message);
|
||||||
|
}
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT previous, author, id, sequence, timestamp, hash, content, signature, sequence_before_author FROM messages WHERE author = ?1 AND sequence >= ?2 ORDER BY sequence',
|
||||||
|
[id, seq ?? 0],
|
||||||
|
sendMessage);
|
||||||
|
ssb.addEventListener('message', function(message_id) {
|
||||||
|
ssb.sqlStream(
|
||||||
|
'SELECT previous, author, id, sequence, timestamp, hash, content, signature, sequence_before_author FROM messages WHERE id = ?1 AND author = ?2',
|
||||||
|
[message_id, id],
|
||||||
|
function (row) {
|
||||||
|
sendMessage(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
113
core/style.css
113
core/style.css
@ -20,6 +20,22 @@ body {
|
|||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:link {
|
||||||
|
color: #268bd2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #2aa198;
|
||||||
|
}
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@ -146,6 +162,51 @@ body {
|
|||||||
.cyan { color: #2aa198; }
|
.cyan { color: #2aa198; }
|
||||||
.green { color: #859900; }
|
.green { color: #859900; }
|
||||||
|
|
||||||
|
#files_header {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#files_hide {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
right: 0;
|
||||||
|
flex: 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#files_show {
|
||||||
|
display: none;
|
||||||
|
padding: 0.25em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filesPane {
|
||||||
|
flex: 1 1 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filesPane.collapsed {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed #files_header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed #files_content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed #files_hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed #files_show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
#files {
|
#files {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -160,3 +221,55 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: #2aa198;
|
background-color: #2aa198;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#files > li.dirty::after {
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
display: none;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 4px;
|
||||||
|
color: black;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip_parent:hover .tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #b4b4b4;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
||||||
|
color: #333;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .85em;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 2px 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#permissions, #permissions_settings {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions_contents {
|
||||||
|
background-color: #444;
|
||||||
|
border-left: 4px solid #fff;
|
||||||
|
border-right: 4px solid #fff;
|
||||||
|
border-bottom: 4px solid #fff;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
93
core/tfrpc.js
Normal file
93
core/tfrpc.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
const k_is_browser = get_is_browser();
|
||||||
|
let g_api = {};
|
||||||
|
let g_next_id = 1;
|
||||||
|
let g_calls = {};
|
||||||
|
|
||||||
|
function get_is_browser() {
|
||||||
|
try { return window !== undefined && console !== undefined; } catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (k_is_browser) {
|
||||||
|
print = console.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
function make_rpc(target, prop, receiver) {
|
||||||
|
return function() {
|
||||||
|
let id = g_next_id++;
|
||||||
|
while (!id || g_calls[id]) {
|
||||||
|
id = g_next_id++;
|
||||||
|
}
|
||||||
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
g_calls[id] = {resolve: resolve, reject: reject};
|
||||||
|
});
|
||||||
|
if (k_is_browser) {
|
||||||
|
window.parent.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}, '*');
|
||||||
|
return promise;
|
||||||
|
} else {
|
||||||
|
return app.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}).then(x => promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(response) {
|
||||||
|
if (k_is_browser) {
|
||||||
|
window.parent.postMessage(response, '*');
|
||||||
|
} else {
|
||||||
|
app.postMessage(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function call_rpc(message) {
|
||||||
|
if (message && message.message === 'tfrpc') {
|
||||||
|
if (message.method) {
|
||||||
|
let method = g_api[message.method];
|
||||||
|
if (method) {
|
||||||
|
let response = {message: 'tfrpc', id: message.id};
|
||||||
|
try {
|
||||||
|
Promise.resolve(method(...message.params)).then(function(result) {
|
||||||
|
response.result = result;
|
||||||
|
send(response);
|
||||||
|
}).catch(function(error) {
|
||||||
|
response.error = error;
|
||||||
|
send(response);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
response.error = error;
|
||||||
|
send(response);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(message.method + ' not found.');
|
||||||
|
}
|
||||||
|
} else if (message.error !== undefined) {
|
||||||
|
if (g_calls[message.id]) {
|
||||||
|
g_calls[message.id].reject(message.error);
|
||||||
|
delete g_calls[message.id];
|
||||||
|
} else {
|
||||||
|
throw new Error(message.id + ' not found to reply.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (g_calls[message.id]) {
|
||||||
|
g_calls[message.id].resolve(message.result);
|
||||||
|
delete g_calls[message.id];
|
||||||
|
} else {
|
||||||
|
throw new Error(message.id + ' not found to reply.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
|
|
||||||
|
export function register(method) {
|
||||||
|
g_api[method.name] = method;
|
||||||
|
}
|
1
deps/codemirror/annotatescrollbar.min.js
vendored
Normal file
1
deps/codemirror/annotatescrollbar.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(t){"use strict";function e(t,e){function i(t){clearTimeout(n.doRedraw),n.doRedraw=setTimeout(function(){n.redraw()},t)}this.cm=t,this.options=e,this.buttonHeight=e.scrollButtonHeight||t.getOption("scrollButtonHeight"),this.annotations=[],this.doRedraw=this.doUpdate=null,this.div=t.getWrapperElement().appendChild(document.createElement("div")),this.div.style.cssText="position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none",this.computeScale();var n=this;t.on("refresh",this.resizeHandler=function(){clearTimeout(n.doUpdate),n.doUpdate=setTimeout(function(){n.computeScale()&&i(20)},100)}),t.on("markerAdded",this.resizeHandler),t.on("markerCleared",this.resizeHandler),!1!==e.listenForChanges&&t.on("changes",this.changeHandler=function(){i(250)})}t.defineExtension("annotateScrollbar",function(t){return new e(this,t="string"==typeof t?{className:t}:t)}),t.defineOption("scrollButtonHeight",0),e.prototype.computeScale=function(){var t=this.cm,t=(t.getWrapperElement().clientHeight-t.display.barHeight-2*this.buttonHeight)/t.getScrollerElement().scrollHeight;if(t!=this.hScale)return this.hScale=t,!0},e.prototype.update=function(t){this.annotations=t,this.redraw()},e.prototype.redraw=function(t){!1!==t&&this.computeScale();var n=this.cm,e=this.hScale,i=document.createDocumentFragment(),o=this.annotations,r=n.getOption("lineWrapping"),a=r&&1.5*n.defaultTextHeight(),s=null,h=null;function l(t,e){var i;return s!=t.line&&(s=t.line,h=n.getLineHandle(t.line),(i=n.getLineHandleVisualStart(h))!=h&&(s=n.getLineNumber(i),h=i)),h.widgets&&h.widgets.length||r&&h.height>a?n.charCoords(t,"local")[e?"top":"bottom"]:n.heightAtLine(h,"local")+(e?0:h.height)}var d=n.lastLine();if(n.display.barWidth)for(var c,p=0;p<o.length;p++){var u=o[p];if(!(u.to.line>d)){for(var m,f,g=c||l(u.from,!0)*e,H=l(u.to,!1)*e;p<o.length-1&&!(o[p+1].to.line>d)&&!(H+.9<(c=l(o[p+1].from,!0)*e));)H=l((u=o[++p]).to,!1)*e;H!=g&&(m=Math.max(H-g,3),(f=i.appendChild(document.createElement("div"))).style.cssText="position: absolute; right: 0px; width: "+Math.max(n.display.barWidth-1,2)+"px; top: "+(g+this.buttonHeight)+"px; height: "+m+"px",f.className=this.options.className,u.id&&f.setAttribute("annotation-id",u.id))}}this.div.textContent="",this.div.appendChild(i)},e.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("changes",this.changeHandler),this.div.parentNode.removeChild(this.div)}});
|
1
deps/codemirror/base16-dark.min.css
vendored
Normal file
1
deps/codemirror/base16-dark.min.css
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.cm-s-base16-dark.CodeMirror{background:#151515;color:#e0e0e0}.cm-s-base16-dark div.CodeMirror-selected{background:#303030}.cm-s-base16-dark .CodeMirror-line::selection,.cm-s-base16-dark .CodeMirror-line>span::selection,.cm-s-base16-dark .CodeMirror-line>span>span::selection{background:rgba(48,48,48,.99)}.cm-s-base16-dark .CodeMirror-line::-moz-selection,.cm-s-base16-dark .CodeMirror-line>span::-moz-selection,.cm-s-base16-dark .CodeMirror-line>span>span::-moz-selection{background:rgba(48,48,48,.99)}.cm-s-base16-dark .CodeMirror-gutters{background:#151515;border-right:0}.cm-s-base16-dark .CodeMirror-guttermarker{color:#ac4142}.cm-s-base16-dark .CodeMirror-guttermarker-subtle{color:#505050}.cm-s-base16-dark .CodeMirror-linenumber{color:#505050}.cm-s-base16-dark .CodeMirror-cursor{border-left:1px solid #b0b0b0}.cm-s-base16-dark.cm-fat-cursor .CodeMirror-cursor{background-color:#8e8d8875!important}.cm-s-base16-dark .cm-animate-fat-cursor{background-color:#8e8d8875!important}.cm-s-base16-dark span.cm-comment{color:#8f5536}.cm-s-base16-dark span.cm-atom{color:#aa759f}.cm-s-base16-dark span.cm-number{color:#aa759f}.cm-s-base16-dark span.cm-attribute,.cm-s-base16-dark span.cm-property{color:#90a959}.cm-s-base16-dark span.cm-keyword{color:#ac4142}.cm-s-base16-dark span.cm-string{color:#f4bf75}.cm-s-base16-dark span.cm-variable{color:#90a959}.cm-s-base16-dark span.cm-variable-2{color:#6a9fb5}.cm-s-base16-dark span.cm-def{color:#d28445}.cm-s-base16-dark span.cm-bracket{color:#e0e0e0}.cm-s-base16-dark span.cm-tag{color:#ac4142}.cm-s-base16-dark span.cm-link{color:#aa759f}.cm-s-base16-dark span.cm-error{background:#ac4142;color:#b0b0b0}.cm-s-base16-dark .CodeMirror-activeline-background{background:#202020}.cm-s-base16-dark .CodeMirror-matchingbracket{text-decoration:underline;color:#fff!important}
|
1
deps/codemirror/codemirror.min.css
vendored
Normal file
1
deps/codemirror/codemirror.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/codemirror.min.js
vendored
Normal file
1
deps/codemirror/codemirror.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/css.min.js
vendored
Normal file
1
deps/codemirror/css.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/dialog.min.css
vendored
Normal file
1
deps/codemirror/dialog.min.css
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.CodeMirror-dialog{position:absolute;left:0;right:0;background:inherit;z-index:15;padding:.1em .8em;overflow:hidden;color:inherit}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{border:none;outline:0;background:0 0;width:20em;color:inherit;font-family:monospace}.CodeMirror-dialog button{font-size:70%}
|
1
deps/codemirror/dialog.min.js
vendored
Normal file
1
deps/codemirror/dialog.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(s){function f(e,o,n){var e=e.getWrapperElement(),t=e.appendChild(document.createElement("div"));return t.className=n?"CodeMirror-dialog CodeMirror-dialog-bottom":"CodeMirror-dialog CodeMirror-dialog-top","string"==typeof o?t.innerHTML=o:t.appendChild(o),s.addClass(e,"dialog-opened"),t}function p(e,o){e.state.currentNotificationClose&&e.state.currentNotificationClose(),e.state.currentNotificationClose=o}s.defineExtension("openDialog",function(e,o,n){n=n||{},p(this,null);var t=f(this,e,n.bottom),i=!1,r=this;function u(e){"string"==typeof e?l.value=e:i||(i=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),r.focus(),n.onClose&&n.onClose(t))}var l=t.getElementsByTagName("input")[0];return l?(l.focus(),n.value&&(l.value=n.value,!1!==n.selectValueOnOpen&&l.select()),n.onInput&&s.on(l,"input",function(e){n.onInput(e,l.value,u)}),n.onKeyUp&&s.on(l,"keyup",function(e){n.onKeyUp(e,l.value,u)}),s.on(l,"keydown",function(e){n&&n.onKeyDown&&n.onKeyDown(e,l.value,u)||((27==e.keyCode||!1!==n.closeOnEnter&&13==e.keyCode)&&(l.blur(),s.e_stop(e),u()),13==e.keyCode&&o(l.value,e))}),!1!==n.closeOnBlur&&s.on(t,"focusout",function(e){null!==e.relatedTarget&&u()})):(e=t.getElementsByTagName("button")[0])&&(s.on(e,"click",function(){u(),r.focus()}),!1!==n.closeOnBlur&&s.on(e,"blur",u),e.focus()),u}),s.defineExtension("openConfirm",function(e,o,n){p(this,null);var t=f(this,e,n&&n.bottom),i=t.getElementsByTagName("button"),r=!1,u=this,l=1;function a(){r||(r=!0,s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t),u.focus())}i[0].focus();for(var c=0;c<i.length;++c){var d=i[c];!function(o){s.on(d,"click",function(e){s.e_preventDefault(e),a(),o&&o(u)})}(o[c]),s.on(d,"blur",function(){--l,setTimeout(function(){l<=0&&a()},200)}),s.on(d,"focus",function(){++l})}}),s.defineExtension("openNotification",function(e,o){p(this,r);var n,t=f(this,e,o&&o.bottom),i=!1,e=o&&void 0!==o.duration?o.duration:5e3;function r(){i||(i=!0,clearTimeout(n),s.rmClass(t.parentNode,"dialog-opened"),t.parentNode.removeChild(t))}return s.on(t,"click",function(e){s.e_preventDefault(e),r()}),e&&(n=setTimeout(r,e)),r})});
|
1
deps/codemirror/htmlmixed.min.js
vendored
Normal file
1
deps/codemirror/htmlmixed.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("../xml/xml"),require("../javascript/javascript"),require("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],t):t(CodeMirror)}(function(m){"use strict";var l={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};var a={};function d(t,e){e=t.match(a[t=e]||(a[t]=new RegExp("\\s+"+t+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")));return e?/^\s*(.*?)\s*$/.exec(e[2])[1]:""}function g(t,e){return new RegExp((e?"^":"")+"</\\s*"+t+"\\s*>","i")}function o(t,e){for(var a in t)for(var n=e[a]||(e[a]=[]),l=t[a],o=l.length-1;0<=o;o--)n.unshift(l[o])}m.defineMode("htmlmixed",function(i,t){var c=m.getMode(i,{name:"xml",htmlMode:!0,multilineTagIndentFactor:t.multilineTagIndentFactor,multilineTagIndentPastTag:t.multilineTagIndentPastTag,allowMissingTagName:t.allowMissingTagName}),s={},e=t&&t.tags,a=t&&t.scriptTypes;if(o(l,s),e&&o(e,s),a)for(var n=a.length-1;0<=n;n--)s.script.unshift(["type",a[n].matches,a[n].mode]);function u(t,e){var a,o,r,n=c.token(t,e.htmlState),l=/\btag\b/.test(n);return l&&!/[<>\s\/]/.test(t.current())&&(a=e.htmlState.tagName&&e.htmlState.tagName.toLowerCase())&&s.hasOwnProperty(a)?e.inTag=a+" ":e.inTag&&l&&/>$/.test(t.current())?(a=/^([\S]+) (.*)/.exec(e.inTag),e.inTag=null,l=">"==t.current()&&function(t,e){for(var a=0;a<t.length;a++){var n=t[a];if(!n[0]||n[1].test(d(e,n[0])))return n[2]}}(s[a[1]],a[2]),l=m.getMode(i,l),o=g(a[1],!0),r=g(a[1],!1),e.token=function(t,e){return t.match(o,!1)?(e.token=u,e.localState=e.localMode=null):(a=t,n=r,t=e.localMode.token(t,e.localState),e=a.current(),-1<(l=e.search(n))?a.backUp(e.length-l):e.match(/<\/?$/)&&(a.backUp(e.length),a.match(n,!1)||a.match(e)),t);var a,n,l},e.localMode=l,e.localState=m.startState(l,c.indent(e.htmlState,"",""))):e.inTag&&(e.inTag+=t.current(),t.eol()&&(e.inTag+=" ")),n}return{startState:function(){return{token:u,inTag:null,localMode:null,localState:null,htmlState:m.startState(c)}},copyState:function(t){var e;return t.localState&&(e=m.copyState(t.localMode,t.localState)),{token:t.token,inTag:t.inTag,localMode:t.localMode,localState:e,htmlState:m.copyState(c,t.htmlState)}},token:function(t,e){return e.token(t,e)},indent:function(t,e,a){return!t.localMode||/^\s*<\//.test(e)?c.indent(t.htmlState,e,a):t.localMode.indent?t.localMode.indent(t.localState,e,a):m.Pass},innerMode:function(t){return{state:t.localState||t.htmlState,mode:t.localMode||c}}}},"xml","javascript","css"),m.defineMIME("text/html","htmlmixed")});
|
1
deps/codemirror/javascript.min.js
vendored
Normal file
1
deps/codemirror/javascript.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/jump-to-line.min.js
vendored
Normal file
1
deps/codemirror/jump-to-line.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror"),require("../dialog/dialog")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../dialog/dialog"],e):e(CodeMirror)}(function(e){"use strict";function l(e,o){var r=Number(o);return/^[-+]/.test(o)?e.getCursor().line+r:r-1}e.defineOption("search",{bottom:!1}),e.commands.jumpToLine=function(t){var e,o,r,s,i,n=t.getCursor();i=(i=e=t).phrase("Jump to line:")+' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">'+i.phrase("(Use line:column or scroll% syntax)")+"</span>",o=t.phrase("Jump to line:"),r=n.line+1+":"+n.ch,s=function(e){var o,r;e&&((o=/^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(e))?t.setCursor(l(t,o[1]),Number(o[2])):(o=/^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(e))?(r=Math.round(t.lineCount()*Number(o[1])/100),/^[-+]/.test(o[1])&&(r=n.line+r+1),t.setCursor(r-1,n.ch)):(o=/^\s*\:?\s*([\+\-]?\d+)\s*/.exec(e))&&t.setCursor(l(t,o[1]),n.ch))},e.openDialog?e.openDialog(i,s,{value:r,selectValueOnOpen:!0,bottom:e.options.search.bottom}):s(prompt(o,r))},e.keyMap.default["Alt-G"]="jumpToLine"});
|
1
deps/codemirror/matchesonscrollbar.min.css
vendored
Normal file
1
deps/codemirror/matchesonscrollbar.min.css
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.CodeMirror-search-match{background:gold;border-top:1px solid orange;border-bottom:1px solid orange;-moz-box-sizing:border-box;box-sizing:border-box;opacity:.5}
|
1
deps/codemirror/matchesonscrollbar.min.js
vendored
Normal file
1
deps/codemirror/matchesonscrollbar.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("./searchcursor"),require("../scroll/annotatescrollbar")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./searchcursor","../scroll/annotatescrollbar"],t):t(CodeMirror)}(function(h){"use strict";function o(t,e,i,o){this.cm=t;var n,s={listenForChanges:!1};for(n in this.options=o)s[n]=o[n];s.className||(s.className="CodeMirror-search-match"),this.annotation=t.annotateScrollbar(s),this.query=e,this.caseFold=i,this.gap={from:t.firstLine(),to:t.lastLine()+1},this.matches=[],this.update=null,this.findMatches(),this.annotation.update(this.matches);var a=this;t.on("change",this.changeHandler=function(t,e){a.onChange(e)})}h.defineExtension("showMatchesOnScrollbar",function(t,e,i){return new o(this,t,e,i=(i="string"==typeof i?{className:i}:i)||{})});function c(t,e,i){return t<=e?t:Math.max(e,t+i)}o.prototype.findMatches=function(){if(this.gap){for(var t=0;t<this.matches.length;t++){if((e=this.matches[t]).from.line>=this.gap.to)break;e.to.line>=this.gap.from&&this.matches.splice(t--,1)}for(var e,i=this.cm.getSearchCursor(this.query,h.Pos(this.gap.from,0),{caseFold:this.caseFold,multiline:this.options.multiline}),o=this.options&&this.options.maxMatches||1e3;i.findNext();){if((e={from:i.from(),to:i.to()}).from.line>=this.gap.to)break;if(this.matches.splice(t++,0,e),this.matches.length>o)break}this.gap=null}},o.prototype.onChange=function(t){var e=t.from.line,i=h.changeEnd(t).line,o=i-t.to.line;if(this.gap?(this.gap.from=Math.min(c(this.gap.from,e,o),t.from.line),this.gap.to=Math.max(c(this.gap.to,e,o),t.from.line)):this.gap={from:t.from.line,to:i+1},o)for(var n=0;n<this.matches.length;n++){var s=this.matches[n],a=c(s.from.line,e,o),a=(a!=s.from.line&&(s.from=h.Pos(a,s.from.ch)),c(s.to.line,e,o));a!=s.to.line&&(s.to=h.Pos(a,s.to.ch))}clearTimeout(this.update);var r=this;this.update=setTimeout(function(){r.updateAfterChange()},250)},o.prototype.updateAfterChange=function(){this.findMatches(),this.annotation.update(this.matches)},o.prototype.clear=function(){this.cm.off("change",this.changeHandler),this.annotation.clear()}});
|
1
deps/codemirror/search.min.js
vendored
Normal file
1
deps/codemirror/search.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/searchcursor.min.js
vendored
Normal file
1
deps/codemirror/searchcursor.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
deps/codemirror/trailingspace.min.js
vendored
Normal file
1
deps/codemirror/trailingspace.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(o){o.defineOption("showTrailingSpace",!1,function(e,i,n){(n=n==o.Init?!1:n)&&!i?e.removeOverlay("trailingspace"):!n&&i&&e.addOverlay({token:function(e){for(var i=e.string.length,n=i;n&&/\s/.test(e.string.charAt(n-1));--n);return n>e.pos?(e.pos=n,null):(e.pos=i,"trailingspace")},name:"trailingspace"})})});
|
1
deps/codemirror/xml.min.js
vendored
Normal file
1
deps/codemirror/xml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
deps/libbacktrace/.gitignore
vendored
Normal file
5
deps/libbacktrace/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*~
|
||||||
|
*.o
|
||||||
|
*.lo
|
||||||
|
*.a
|
||||||
|
*.la
|
9286
deps/libbacktrace/Isaac.Newton-Opticks.txt
vendored
Normal file
9286
deps/libbacktrace/Isaac.Newton-Opticks.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
29
deps/libbacktrace/LICENSE
vendored
Normal file
29
deps/libbacktrace/LICENSE
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (C) 2012-2016 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
|
||||||
|
# (1) Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
# (2) Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
|
||||||
|
# (3) The name of the author may not be used to
|
||||||
|
# endorse or promote products derived from this software without
|
||||||
|
# specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||||
|
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||||
|
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||||
|
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
591
deps/libbacktrace/Makefile.am
vendored
Normal file
591
deps/libbacktrace/Makefile.am
vendored
Normal file
@ -0,0 +1,591 @@
|
|||||||
|
# Makefile.am -- Backtrace Makefile.
|
||||||
|
# Copyright (C) 2012-2021 Free Software Foundation, Inc.
|
||||||
|
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are
|
||||||
|
# met:
|
||||||
|
|
||||||
|
# (1) Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
# (2) Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in
|
||||||
|
# the documentation and/or other materials provided with the
|
||||||
|
# distribution.
|
||||||
|
|
||||||
|
# (3) The name of the author may not be used to
|
||||||
|
# endorse or promote products derived from this software without
|
||||||
|
# specific prior written permission.
|
||||||
|
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||||
|
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||||
|
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||||
|
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
ACLOCAL_AMFLAGS = -I config
|
||||||
|
|
||||||
|
AM_CPPFLAGS =
|
||||||
|
|
||||||
|
AM_CFLAGS = $(EXTRA_FLAGS) $(WARN_FLAGS) $(PIC_FLAG)
|
||||||
|
|
||||||
|
include_HEADERS = backtrace.h backtrace-supported.h
|
||||||
|
|
||||||
|
lib_LTLIBRARIES = libbacktrace.la
|
||||||
|
|
||||||
|
libbacktrace_la_SOURCES = \
|
||||||
|
backtrace.h \
|
||||||
|
atomic.c \
|
||||||
|
dwarf.c \
|
||||||
|
fileline.c \
|
||||||
|
internal.h \
|
||||||
|
posix.c \
|
||||||
|
print.c \
|
||||||
|
sort.c \
|
||||||
|
state.c
|
||||||
|
|
||||||
|
BACKTRACE_FILES = \
|
||||||
|
backtrace.c \
|
||||||
|
simple.c \
|
||||||
|
nounwind.c
|
||||||
|
|
||||||
|
FORMAT_FILES = \
|
||||||
|
elf.c \
|
||||||
|
macho.c \
|
||||||
|
pecoff.c \
|
||||||
|
unknown.c \
|
||||||
|
xcoff.c
|
||||||
|
|
||||||
|
VIEW_FILES = \
|
||||||
|
read.c \
|
||||||
|
mmapio.c
|
||||||
|
|
||||||
|
ALLOC_FILES = \
|
||||||
|
alloc.c \
|
||||||
|
mmap.c
|
||||||
|
|
||||||
|
EXTRA_libbacktrace_la_SOURCES = \
|
||||||
|
$(BACKTRACE_FILES) \
|
||||||
|
$(FORMAT_FILES) \
|
||||||
|
$(VIEW_FILES) \
|
||||||
|
$(ALLOC_FILES)
|
||||||
|
|
||||||
|
libbacktrace_la_LIBADD = \
|
||||||
|
$(BACKTRACE_FILE) \
|
||||||
|
$(FORMAT_FILE) \
|
||||||
|
$(VIEW_FILE) \
|
||||||
|
$(ALLOC_FILE)
|
||||||
|
|
||||||
|
libbacktrace_la_DEPENDENCIES = $(libbacktrace_la_LIBADD)
|
||||||
|
|
||||||
|
# Testsuite.
|
||||||
|
|
||||||
|
# Add a test to this variable if you want it to be built as a program,
|
||||||
|
# with SOURCES, etc.
|
||||||
|
check_PROGRAMS =
|
||||||
|
|
||||||
|
# Add a test to this variable if you want it to be run.
|
||||||
|
TESTS =
|
||||||
|
|
||||||
|
# Add a test to this variable if you want it to be built as a Makefile
|
||||||
|
# target and run.
|
||||||
|
MAKETESTS =
|
||||||
|
|
||||||
|
# Add a test to this variable if you want it to be built as a program,
|
||||||
|
# with SOURCES, etc., and run.
|
||||||
|
BUILDTESTS =
|
||||||
|
|
||||||
|
# Add a file to this variable if you want it to be built for testing.
|
||||||
|
check_DATA =
|
||||||
|
|
||||||
|
# Flags to use when compiling test programs.
|
||||||
|
libbacktrace_TEST_CFLAGS = $(EXTRA_FLAGS) $(WARN_FLAGS) -g
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
|
||||||
|
%.dSYM: %
|
||||||
|
$(DSYMUTIL) $<
|
||||||
|
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if NATIVE
|
||||||
|
check_LTLIBRARIES = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
libbacktrace_alloc_la_SOURCES = $(libbacktrace_la_SOURCES)
|
||||||
|
libbacktrace_alloc_la_LIBADD = $(BACKTRACE_FILE) $(FORMAT_FILE) read.lo alloc.lo
|
||||||
|
|
||||||
|
libbacktrace_alloc_la_DEPENDENCIES = $(libbacktrace_alloc_la_LIBADD)
|
||||||
|
|
||||||
|
check_LTLIBRARIES += libbacktrace_noformat.la
|
||||||
|
|
||||||
|
libbacktrace_noformat_la_SOURCES = $(libbacktrace_la_SOURCES)
|
||||||
|
libbacktrace_noformat_la_LIBADD = $(BACKTRACE_FILE) $(VIEW_FILE) $(ALLOC_FILE)
|
||||||
|
|
||||||
|
libbacktrace_noformat_la_DEPENDENCIES = $(libbacktrace_noformat_la_LIBADD)
|
||||||
|
|
||||||
|
if HAVE_ELF
|
||||||
|
if HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
TEST_BUILD_ID_DIR=$(abs_builddir)/usr/lib/debug/.build-id/
|
||||||
|
|
||||||
|
check_LTLIBRARIES += libbacktrace_elf_for_test.la
|
||||||
|
|
||||||
|
libbacktrace_elf_for_test_la_SOURCES = $(libbacktrace_la_SOURCES)
|
||||||
|
libbacktrace_elf_for_test_la_LIBADD = $(BACKTRACE_FILE) elf_for_test.lo \
|
||||||
|
$(VIEW_FILE) $(ALLOC_FILE)
|
||||||
|
|
||||||
|
elf_for_test.c: elf.c
|
||||||
|
SEARCH='^#define SYSTEM_BUILD_ID_DIR.*$$'; \
|
||||||
|
REPLACE="#define SYSTEM_BUILD_ID_DIR \"$(TEST_BUILD_ID_DIR)\""; \
|
||||||
|
$(SED) "s%$$SEARCH%$$REPLACE%" \
|
||||||
|
$< \
|
||||||
|
> $@.tmp
|
||||||
|
mv $@.tmp $@
|
||||||
|
|
||||||
|
endif HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
endif HAVE_ELF
|
||||||
|
|
||||||
|
elf_%.c: elf.c
|
||||||
|
SEARCH='#error "Unknown BACKTRACE_ELF_SIZE"'; \
|
||||||
|
REPLACE='#undef BACKTRACE_ELF_SIZE\
|
||||||
|
#define BACKTRACE_ELF_SIZE'; \
|
||||||
|
$(SED) "s/^$$SEARCH\$$/$$REPLACE $*/" \
|
||||||
|
$< \
|
||||||
|
> $@.tmp
|
||||||
|
mv $@.tmp $@
|
||||||
|
|
||||||
|
xcoff_%.c: xcoff.c
|
||||||
|
SEARCH='#error "Unknown BACKTRACE_XCOFF_SIZE"'; \
|
||||||
|
REPLACE='#undef BACKTRACE_XCOFF_SIZE\
|
||||||
|
#define BACKTRACE_XCOFF_SIZE'; \
|
||||||
|
$(SED) "s/^$$SEARCH\$$/$$REPLACE $*/" \
|
||||||
|
$< \
|
||||||
|
> $@.tmp
|
||||||
|
mv $@.tmp $@
|
||||||
|
|
||||||
|
test_elf_32_SOURCES = test_format.c testlib.c
|
||||||
|
test_elf_32_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_elf_32_LDADD = libbacktrace_noformat.la elf_32.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_elf_32
|
||||||
|
|
||||||
|
test_elf_64_SOURCES = test_format.c testlib.c
|
||||||
|
test_elf_64_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_elf_64_LDADD = libbacktrace_noformat.la elf_64.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_elf_64
|
||||||
|
|
||||||
|
test_macho_SOURCES = test_format.c testlib.c
|
||||||
|
test_macho_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_macho_LDADD = libbacktrace_noformat.la macho.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_macho
|
||||||
|
|
||||||
|
test_xcoff_32_SOURCES = test_format.c testlib.c
|
||||||
|
test_xcoff_32_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_xcoff_32_LDADD = libbacktrace_noformat.la xcoff_32.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_xcoff_32
|
||||||
|
|
||||||
|
test_xcoff_64_SOURCES = test_format.c testlib.c
|
||||||
|
test_xcoff_64_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_xcoff_64_LDADD = libbacktrace_noformat.la xcoff_64.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_xcoff_64
|
||||||
|
|
||||||
|
test_pecoff_SOURCES = test_format.c testlib.c
|
||||||
|
test_pecoff_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_pecoff_LDADD = libbacktrace_noformat.la pecoff.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_pecoff
|
||||||
|
|
||||||
|
test_unknown_SOURCES = test_format.c testlib.c
|
||||||
|
test_unknown_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
test_unknown_LDADD = libbacktrace_noformat.la unknown.lo
|
||||||
|
|
||||||
|
BUILDTESTS += test_unknown
|
||||||
|
|
||||||
|
unittest_SOURCES = unittest.c testlib.c
|
||||||
|
unittest_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
unittest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += unittest
|
||||||
|
|
||||||
|
unittest_alloc_SOURCES = $(unittest_SOURCES)
|
||||||
|
unittest_alloc_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
unittest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
BUILDTESTS += unittest_alloc
|
||||||
|
|
||||||
|
check_LTLIBRARIES += libbacktrace_instrumented_alloc.la
|
||||||
|
|
||||||
|
libbacktrace_instrumented_alloc_la_SOURCES = $(libbacktrace_la_SOURCES)
|
||||||
|
libbacktrace_instrumented_alloc_la_LIBADD = $(BACKTRACE_FILE) $(FORMAT_FILE) \
|
||||||
|
read.lo instrumented_alloc.lo
|
||||||
|
|
||||||
|
libbacktrace_instrumented_alloc_la_DEPENDENCIES = \
|
||||||
|
$(libbacktrace_instrumented_alloc_la_LIBADD)
|
||||||
|
|
||||||
|
instrumented_alloc.lo: alloc.c
|
||||||
|
|
||||||
|
allocfail_SOURCES = allocfail.c testlib.c
|
||||||
|
allocfail_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
allocfail_LDADD = libbacktrace_instrumented_alloc.la
|
||||||
|
|
||||||
|
check_PROGRAMS += allocfail
|
||||||
|
|
||||||
|
allocfail.sh: allocfail
|
||||||
|
|
||||||
|
TESTS += allocfail.sh
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += allocfail.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if HAVE_ELF
|
||||||
|
if HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
b2test_SOURCES = $(btest_SOURCES)
|
||||||
|
b2test_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
b2test_LDFLAGS = -Wl,--build-id
|
||||||
|
b2test_LDADD = libbacktrace_elf_for_test.la
|
||||||
|
|
||||||
|
check_PROGRAMS += b2test
|
||||||
|
MAKETESTS += b2test_buildid
|
||||||
|
|
||||||
|
if HAVE_DWZ
|
||||||
|
|
||||||
|
b3test_SOURCES = $(btest_SOURCES)
|
||||||
|
b3test_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
b3test_LDFLAGS = -Wl,--build-id
|
||||||
|
b3test_LDADD = libbacktrace_elf_for_test.la
|
||||||
|
|
||||||
|
check_PROGRAMS += b3test
|
||||||
|
MAKETESTS += b3test_dwz_buildid
|
||||||
|
|
||||||
|
endif HAVE_DWZ
|
||||||
|
|
||||||
|
endif HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
endif HAVE_ELF
|
||||||
|
|
||||||
|
btest_SOURCES = btest.c testlib.c
|
||||||
|
btest_CFLAGS = $(libbacktrace_TEST_CFLAGS) -O
|
||||||
|
btest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += btest
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += btest.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if HAVE_ELF
|
||||||
|
|
||||||
|
btest_lto_SOURCES = btest.c testlib.c
|
||||||
|
btest_lto_CFLAGS = $(libbacktrace_TEST_CFLAGS) -O -flto
|
||||||
|
btest_lto_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += btest_lto
|
||||||
|
|
||||||
|
endif HAVE_ELF
|
||||||
|
|
||||||
|
btest_alloc_SOURCES = $(btest_SOURCES)
|
||||||
|
btest_alloc_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
btest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
BUILDTESTS += btest_alloc
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += btest_alloc.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if HAVE_DWZ
|
||||||
|
|
||||||
|
%_dwz: %
|
||||||
|
rm -f $@ $@_common.debug
|
||||||
|
cp $< $@_1
|
||||||
|
cp $< $@_2
|
||||||
|
if $(DWZ) -m $@_common.debug $@_1 $@_2; then \
|
||||||
|
rm -f $@_2; \
|
||||||
|
mv $@_1 $@; \
|
||||||
|
else \
|
||||||
|
echo "Ignoring dwz errors, assuming that test passes"; \
|
||||||
|
cp $< $@; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
MAKETESTS += btest_dwz
|
||||||
|
|
||||||
|
if HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
MAKETESTS += btest_dwz_gnudebuglink
|
||||||
|
|
||||||
|
endif HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
endif HAVE_DWZ
|
||||||
|
|
||||||
|
stest_SOURCES = stest.c
|
||||||
|
stest_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
stest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += stest
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += stest.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
stest_alloc_SOURCES = $(stest_SOURCES)
|
||||||
|
stest_alloc_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
stest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
BUILDTESTS += stest_alloc
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += stest_alloc.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if HAVE_ELF
|
||||||
|
|
||||||
|
ztest_SOURCES = ztest.c testlib.c
|
||||||
|
ztest_CFLAGS = $(libbacktrace_TEST_CFLAGS) -DSRCDIR=\"$(srcdir)\"
|
||||||
|
ztest_LDADD = libbacktrace.la
|
||||||
|
ztest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
if HAVE_ZLIB
|
||||||
|
ztest_LDADD += -lz
|
||||||
|
ztest_alloc_LDADD += -lz
|
||||||
|
endif
|
||||||
|
ztest_LDADD += $(CLOCK_GETTIME_LINK)
|
||||||
|
ztest_alloc_LDADD += $(CLOCK_GETTIME_LINK)
|
||||||
|
|
||||||
|
BUILDTESTS += ztest
|
||||||
|
|
||||||
|
ztest_alloc_SOURCES = $(ztest_SOURCES)
|
||||||
|
ztest_alloc_CFLAGS = $(ztest_CFLAGS)
|
||||||
|
|
||||||
|
BUILDTESTS += ztest_alloc
|
||||||
|
|
||||||
|
endif HAVE_ELF
|
||||||
|
|
||||||
|
edtest_SOURCES = edtest.c edtest2_build.c testlib.c
|
||||||
|
edtest_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
edtest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += edtest
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += edtest.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
edtest_alloc_SOURCES = $(edtest_SOURCES)
|
||||||
|
edtest_alloc_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
edtest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += edtest_alloc.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
BUILDTESTS += edtest_alloc
|
||||||
|
|
||||||
|
edtest2_build.c: gen_edtest2_build; @true
|
||||||
|
gen_edtest2_build: $(srcdir)/edtest2.c
|
||||||
|
cat $(srcdir)/edtest2.c > tmp-edtest2_build.c
|
||||||
|
$(SHELL) $(srcdir)/move-if-change tmp-edtest2_build.c edtest2_build.c
|
||||||
|
echo timestamp > $@
|
||||||
|
|
||||||
|
if HAVE_PTHREAD
|
||||||
|
|
||||||
|
BUILDTESTS += ttest
|
||||||
|
|
||||||
|
ttest_SOURCES = ttest.c testlib.c
|
||||||
|
ttest_CFLAGS = $(libbacktrace_TEST_CFLAGS) -pthread
|
||||||
|
ttest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += ttest.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
BUILDTESTS += ttest_alloc
|
||||||
|
|
||||||
|
ttest_alloc_SOURCES = $(ttest_SOURCES)
|
||||||
|
ttest_alloc_CFLAGS = $(ttest_CFLAGS)
|
||||||
|
ttest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += ttest_alloc.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
endif HAVE_PTHREAD
|
||||||
|
|
||||||
|
if HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
MAKETESTS += btest_gnudebuglink
|
||||||
|
|
||||||
|
%_gnudebuglink: %
|
||||||
|
$(OBJCOPY) --only-keep-debug $< $@.debug
|
||||||
|
$(OBJCOPY) --strip-debug --add-gnu-debuglink=$@.debug $< $@
|
||||||
|
|
||||||
|
endif HAVE_OBJCOPY_DEBUGLINK
|
||||||
|
|
||||||
|
%_buildid: %
|
||||||
|
./install-debuginfo-for-buildid.sh \
|
||||||
|
"$(TEST_BUILD_ID_DIR)" \
|
||||||
|
$<
|
||||||
|
$(OBJCOPY) --strip-debug $< $@
|
||||||
|
|
||||||
|
if HAVE_COMPRESSED_DEBUG
|
||||||
|
|
||||||
|
ctestg_SOURCES = btest.c testlib.c
|
||||||
|
ctestg_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
ctestg_LDFLAGS = -Wl,--compress-debug-sections=zlib-gnu
|
||||||
|
ctestg_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
ctesta_SOURCES = btest.c testlib.c
|
||||||
|
ctesta_CFLAGS = $(libbacktrace_TEST_CFLAGS)
|
||||||
|
ctesta_LDFLAGS = -Wl,--compress-debug-sections=zlib-gabi
|
||||||
|
ctesta_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += ctestg ctesta
|
||||||
|
|
||||||
|
ctestg_alloc_SOURCES = $(ctestg_SOURCES)
|
||||||
|
ctestg_alloc_CFLAGS = $(ctestg_CFLAGS)
|
||||||
|
ctestg_alloc_LDFLAGS = $(ctestg_LDFLAGS)
|
||||||
|
ctestg_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
ctesta_alloc_SOURCES = $(ctesta_SOURCES)
|
||||||
|
ctesta_alloc_CFLAGS = $(ctesta_CFLAGS)
|
||||||
|
ctesta_alloc_LDFLAGS = $(ctesta_LDFLAGS)
|
||||||
|
ctesta_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
BUILDTESTS += ctestg_alloc ctesta_alloc
|
||||||
|
|
||||||
|
endif
|
||||||
|
|
||||||
|
if HAVE_DWARF5
|
||||||
|
|
||||||
|
dwarf5_SOURCES = btest.c testlib.c
|
||||||
|
dwarf5_CFLAGS = $(libbacktrace_TEST_CFLAGS) -gdwarf-5
|
||||||
|
dwarf5_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += dwarf5
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += dwarf5.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
dwarf5_alloc_SOURCES = $(dwarf5_SOURCES)
|
||||||
|
dwarf5_alloc_CFLAGS = $(dwarf5_CFLAGS)
|
||||||
|
dwarf5_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
BUILDTESTS += dwarf5_alloc
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += dwarf5_alloc.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
endif
|
||||||
|
|
||||||
|
mtest_SOURCES = mtest.c testlib.c
|
||||||
|
mtest_CFLAGS = $(libbacktrace_TEST_CFLAGS) -O
|
||||||
|
mtest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
BUILDTESTS += mtest
|
||||||
|
|
||||||
|
if USE_DSYMUTIL
|
||||||
|
check_DATA += mtest.dSYM
|
||||||
|
endif USE_DSYMUTIL
|
||||||
|
|
||||||
|
if HAVE_MINIDEBUG
|
||||||
|
|
||||||
|
MAKETESTS += mtest_minidebug
|
||||||
|
|
||||||
|
%_minidebug: %
|
||||||
|
$(NM) -D $< -P --defined-only | $(AWK) '{ print $$1 }' | sort > $<.dsyms
|
||||||
|
$(NM) $< -P --defined-only | $(AWK) '{ if ($$2 == "T" || $$2 == "t" || $$2 == "D") print $$1 }' | sort > $<.fsyms
|
||||||
|
$(COMM) -13 $<.dsyms $<.fsyms > $<.keepsyms
|
||||||
|
$(OBJCOPY) --only-keep-debug $< $<.dbg
|
||||||
|
$(OBJCOPY) -S --remove-section .gdb_index --remove-section .comment --keep-symbols=$<.keepsyms $<.dbg $<.mdbg
|
||||||
|
$(OBJCOPY) --strip-all --remove-section ..comment $< $<.strip
|
||||||
|
rm -f $<.mdbg.xz
|
||||||
|
$(XZ) $<.mdbg
|
||||||
|
$(OBJCOPY) --add-section .gnu_debugdata=$<.mdbg.xz $<.strip
|
||||||
|
mv $<.strip $@
|
||||||
|
|
||||||
|
endif HAVE_MINIDEBUG
|
||||||
|
|
||||||
|
endif NATIVE
|
||||||
|
|
||||||
|
if HAVE_ELF
|
||||||
|
|
||||||
|
xztest_SOURCES = xztest.c testlib.c
|
||||||
|
xztest_CFLAGS = $(libbacktrace_TEST_CFLAGS) -DSRCDIR=\"$(srcdir)\"
|
||||||
|
xztest_LDADD = libbacktrace.la
|
||||||
|
|
||||||
|
xztest_alloc_SOURCES = $(xztest_SOURCES)
|
||||||
|
xztest_alloc_CFLAGS = $(xztest_CFLAGS)
|
||||||
|
xztest_alloc_LDADD = libbacktrace_alloc.la
|
||||||
|
|
||||||
|
if HAVE_LIBLZMA
|
||||||
|
xztest_LDADD += -llzma
|
||||||
|
xztest_alloc_LDADD += -llzma
|
||||||
|
endif
|
||||||
|
|
||||||
|
xztest_LDADD += $(CLOCK_GETTIME_LINK)
|
||||||
|
xztest_alloc_LDADD += $(CLOCK_GETTIME_LINK)
|
||||||
|
|
||||||
|
BUILDTESTS += xztest xztest_alloc
|
||||||
|
|
||||||
|
endif HAVE_ELF
|
||||||
|
|
||||||
|
check_PROGRAMS += $(BUILDTESTS)
|
||||||
|
|
||||||
|
TESTS += $(MAKETESTS) $(BUILDTESTS)
|
||||||
|
|
||||||
|
CLEANFILES = \
|
||||||
|
$(MAKETESTS) $(BUILDTESTS) *.debug elf_for_test.c edtest2_build.c \
|
||||||
|
gen_edtest2_build \
|
||||||
|
*.dsyms *.fsyms *.keepsyms *.dbg *.mdbg *.mdbg.xz *.strip
|
||||||
|
|
||||||
|
clean-local:
|
||||||
|
-rm -rf usr
|
||||||
|
|
||||||
|
# We can't use automake's automatic dependency tracking, because it
|
||||||
|
# breaks when using bootstrap-lean. Automatic dependency tracking
|
||||||
|
# with GCC bootstrap will cause some of the objects to depend on
|
||||||
|
# header files in prev-gcc/include, e.g., stddef.h and stdarg.h. When
|
||||||
|
# using bootstrap-lean, prev-gcc is removed after each stage. When
|
||||||
|
# running "make install", those header files will be gone, causing the
|
||||||
|
# library to be rebuilt at install time. That may not succeed.
|
||||||
|
|
||||||
|
# These manual dependencies do not include dependencies on unwind.h,
|
||||||
|
# even though that is part of GCC, because where to find it depends on
|
||||||
|
# whether we are being built as a host library or a target library.
|
||||||
|
|
||||||
|
alloc.lo: config.h backtrace.h internal.h
|
||||||
|
backtrace.lo: config.h backtrace.h internal.h
|
||||||
|
btest.lo: filenames.h backtrace.h backtrace-supported.h
|
||||||
|
dwarf.lo: config.h filenames.h backtrace.h internal.h
|
||||||
|
elf.lo: config.h backtrace.h internal.h
|
||||||
|
fileline.lo: config.h backtrace.h internal.h
|
||||||
|
macho.lo: config.h backtrace.h internal.h
|
||||||
|
mmap.lo: config.h backtrace.h internal.h
|
||||||
|
mmapio.lo: config.h backtrace.h internal.h
|
||||||
|
mtest.lo: backtrace.h backtrace-supported.h
|
||||||
|
nounwind.lo: config.h internal.h
|
||||||
|
pecoff.lo: config.h backtrace.h internal.h
|
||||||
|
posix.lo: config.h backtrace.h internal.h
|
||||||
|
print.lo: config.h backtrace.h internal.h
|
||||||
|
read.lo: config.h backtrace.h internal.h
|
||||||
|
simple.lo: config.h backtrace.h internal.h
|
||||||
|
sort.lo: config.h backtrace.h internal.h
|
||||||
|
stest.lo: config.h backtrace.h internal.h
|
||||||
|
state.lo: config.h backtrace.h backtrace-supported.h internal.h
|
||||||
|
unknown.lo: config.h backtrace.h internal.h
|
||||||
|
xcoff.lo: config.h backtrace.h internal.h
|
||||||
|
xztest.lo: config.h backtrace.h backtrace-supported.h internal.h testlib.h
|
||||||
|
ztest.lo: config.h backtrace.h backtrace-supported.h internal.h testlib.h
|
||||||
|
|
2544
deps/libbacktrace/Makefile.in
vendored
Normal file
2544
deps/libbacktrace/Makefile.in
vendored
Normal file
File diff suppressed because it is too large
Load Diff
36
deps/libbacktrace/README.md
vendored
Normal file
36
deps/libbacktrace/README.md
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# libbacktrace
|
||||||
|
A C library that may be linked into a C/C++ program to produce symbolic backtraces
|
||||||
|
|
||||||
|
Initially written by Ian Lance Taylor <iant@golang.org>.
|
||||||
|
|
||||||
|
This is version 1.0.
|
||||||
|
It is likely that this will always be version 1.0.
|
||||||
|
|
||||||
|
The libbacktrace library may be linked into a program or library and
|
||||||
|
used to produce symbolic backtraces.
|
||||||
|
Sample uses would be to print a detailed backtrace when an error
|
||||||
|
occurs or to gather detailed profiling information.
|
||||||
|
In general the functions provided by this library are async-signal-safe,
|
||||||
|
meaning that they may be safely called from a signal handler.
|
||||||
|
|
||||||
|
The libbacktrace library is provided under a BSD license.
|
||||||
|
See the source files for the exact license text.
|
||||||
|
|
||||||
|
The public functions are declared and documented in the header file
|
||||||
|
backtrace.h, which should be #include'd by a user of the library.
|
||||||
|
|
||||||
|
Building libbacktrace will generate a file backtrace-supported.h,
|
||||||
|
which a user of the library may use to determine whether backtraces
|
||||||
|
will work.
|
||||||
|
See the source file backtrace-supported.h.in for the macros that it
|
||||||
|
defines.
|
||||||
|
|
||||||
|
As of October 2020, libbacktrace supports ELF, PE/COFF, Mach-O, and
|
||||||
|
XCOFF executables with DWARF debugging information.
|
||||||
|
In other words, it supports GNU/Linux, *BSD, macOS, Windows, and AIX.
|
||||||
|
The library is written to make it straightforward to add support for
|
||||||
|
other object file and debugging formats.
|
||||||
|
|
||||||
|
The library relies on the C++ unwind API defined at
|
||||||
|
https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html
|
||||||
|
This API is provided by GCC and clang.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user