Compare commits
644 Commits
Author | SHA1 | Date | |
---|---|---|---|
9cbe895cb8 | |||
b0b0f74e83 | |||
d9eaa92c37 | |||
566d07117e | |||
2bffdb1168 | |||
1359b48c9f | |||
a69fb5eeac | |||
38e313350e | |||
5052dc04f2 | |||
9ef3a3aca0 | |||
7b91a2ec37 | |||
2926f855a1 | |||
639419db60 | |||
54747c127c | |||
791c3dd787 | |||
b00d75ab7c | |||
956ea0df56 | |||
30014040e7 | |||
ab055c3394 | |||
1e37eeea05 | |||
84aec0278d | |||
06642f58c5 | |||
e6d44b32f4 | |||
1f3f6e2b92 | |||
8f2d3e3bcd | |||
2df2fc5792 | |||
20b0337e0a | |||
e86b9dae48 | |||
71de897419 | |||
3edfaf9137 | |||
19c1784864 | |||
0d9fac7363 | |||
2fb91fccc0 | |||
24e1ab12ab | |||
10ea885d8d | |||
ec65faa12d | |||
53692a1ea8 | |||
ebef51b4ea | |||
a94d6f9271 | |||
3d2c88c201 | |||
bdeee7fc0e | |||
33a037e0ea | |||
2dc2d9ebf6 | |||
9748f0ed8b | |||
d6be2f7d54 | |||
63615747a7 | |||
fbb657a85c | |||
bdac0c7879 | |||
54dde76a8a | |||
2bbe22bc7a | |||
ad8532f7ac | |||
602941104e | |||
d38b41687c | |||
08125cd1e8 | |||
2ce2097a3f | |||
a5da17e1b1 | |||
2b0962f087 | |||
37173cce4c | |||
37edbd9824 | |||
a32bb02223 | |||
2ab1b84432 | |||
52ae19220c | |||
10bfa65a4e | |||
2a3b1a1e33 | |||
f74f4f6da9 | |||
12a8b7a058 | |||
400f07660f | |||
d532795b7f | |||
6064ed6a3a | |||
2c1a43df2e | |||
bf72782c9f | |||
63dcab30c3 | |||
50e48af7c4 | |||
9127a18ff0 | |||
61ff466908 | |||
1c10768aa4 | |||
992b123853 | |||
f736756b20 | |||
28d73f5b37 | |||
262b0e5e52 | |||
1e3807bcb9 | |||
2ed3295f77 | |||
8c9d687d50 | |||
b8b694864e | |||
961109635b | |||
86bc46a11e | |||
a6a6fe75ec | |||
f55f863867 | |||
4ce988d00b | |||
1548a8a852 | |||
a9551b057b | |||
88c7d91858 | |||
53cb80ebf7 | |||
1f67343d75 | |||
4bea8bb6ba | |||
8e1461b3f1 | |||
90b513d070 | |||
8a2d3d4669 | |||
1741403206 | |||
980db880cc | |||
507a62539d | |||
6b5d73ed5c | |||
1f77df7a90 | |||
fa87462405 | |||
a5f9f927e6 | |||
b35d74ce36 | |||
ac60be14a5 | |||
beda047eb0 | |||
f6742bebf3 | |||
7f334ad783 | |||
ffda896308 | |||
b2fbe9dfac | |||
6d6c41bffa | |||
e04d137af5 | |||
ec52e62908 | |||
6104af0d70 | |||
0ca05e297d | |||
e0dcec074c | |||
a8cecb5c64 | |||
582ee0e4d7 | |||
0ba54c2b7b | |||
3c288f7f68 | |||
c692b1b1f8 | |||
7091b6e6a5 | |||
48cd08e095 | |||
ef7f9db9c4 | |||
0092f24fb9 | |||
f9db1a7acf | |||
da75ad9337 | |||
7318ddd70e | |||
ab75ec07f8 | |||
0a6b842179 | |||
5d5ff121f9 | |||
adefa76dfd | |||
2420869e7f | |||
f841ca4399 | |||
433db904cd | |||
c067623740 | |||
dab7050899 | |||
77df158178 | |||
0af1bcf110 | |||
e05302ac99 | |||
ce6cc82d64 | |||
85a2bc3f0f | |||
3285d93576 | |||
0f11f497ed | |||
45a5202456 | |||
ce0b4de5a1 | |||
134b2556ad | |||
67d34bf70e | |||
73863f9418 | |||
0cbc1a650b | |||
9248dfd97e | |||
b8f54f324f | |||
3269c7ca45 | |||
8a1b4cceec | |||
7cd925feca | |||
f6ae15c4dc | |||
6ed057089b | |||
a5ba014736 | |||
4d4cc92150 | |||
3b00b31e87 | |||
3c687dc780 | |||
987b2d539a | |||
80a1e94da4 | |||
69253432b8 | |||
53e4f4341c | |||
6ff33191bb | |||
513eb88a53 | |||
3506d9dec1 | |||
c09e043812 | |||
4c01f23ee8 | |||
ff06e91ac8 | |||
8ed359327c | |||
a66a70324d | |||
67fbbd4a8d | |||
235fc9b8f9 | |||
f257cccded | |||
5342ddb2bd | |||
7cba1b21ad | |||
120ed36552 | |||
a9f6593979 | |||
ca6d042ed6 | |||
ae4c2aef69 | |||
ed1c85288c | |||
71151a511d | |||
7f35f01b88 | |||
1d13c25ded | |||
09ddfffa6b | |||
d9aee6d05f | |||
94d7d2e3e0 | |||
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 |
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.}
|
|
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM bitnami/minideb:bullseye AS build
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
libc6-dev \
|
||||||
|
libssl-dev \
|
||||||
|
make
|
||||||
|
|
||||||
|
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/>.
|
|
||||||
|
469
Makefile
469
Makefile
@ -1,63 +1,147 @@
|
|||||||
|
.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 androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
|
||||||
|
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-exceptions \
|
||||||
|
-g
|
||||||
|
LDFLAGS += -Wl,--gc-sections
|
||||||
|
|
||||||
ifneq ($(UNUSED),)
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
COMMON_LDFLAGS += -Wl,-print-gc-sections
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
|
||||||
|
ANDROID_NDK_API_VERSION := 31
|
||||||
|
ANDROID_MIN_SDK_VERSION := 26
|
||||||
|
|
||||||
|
ANDROID_ARM64_TARGETS := \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androidrelease/tildefriends
|
||||||
|
ANDROID_X86_64_TARGETS := \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_TARGETS := \
|
||||||
|
$(ANDROID_X86_64_TARGETS) \
|
||||||
|
$(ANDROID_ARM64_TARGETS)
|
||||||
|
|
||||||
|
DEBUG_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/windebug/tildefriends \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androiddebug-x86_64/tildefriends
|
||||||
|
RELEASE_TARGETS := \
|
||||||
|
out/release/tildefriends \
|
||||||
|
out/winrelease/tildefriends \
|
||||||
|
out/androidrelease/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||||
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
|
||||||
|
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
|
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
|
-fPIC \
|
||||||
|
-fomit-frame-pointer \
|
||||||
|
-fno-asynchronous-unwind-tables
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||||
|
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||||
|
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
||||||
|
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||||
|
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
|
||||||
|
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
||||||
|
windebug winrelease: AS = $(CC)
|
||||||
|
windebug winrelease: CFLAGS += \
|
||||||
|
-D_WIN32_WINNT=0x0A00 \
|
||||||
|
-DWINVER=0x0A00 \
|
||||||
|
-DNTDDI_VERSION=NTDDI_WIN10 \
|
||||||
|
-Ideps/openssl/mingw64/include
|
||||||
|
windebug winrelease: LDFLAGS += \
|
||||||
|
-static \
|
||||||
|
-lm \
|
||||||
|
-Ldeps/openssl/mingw64/lib
|
||||||
|
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
||||||
|
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||||
|
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
||||||
|
$(ANDROID_TARGETS): AS = $(CC)
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
||||||
|
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||||
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
|
|
||||||
|
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
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||||
COMMON_CFLAGS += -DNDEBUG -O3
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
||||||
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/zlib \
|
||||||
|
-Ideps/zlib/contrib/minizip \
|
||||||
|
-Ideps/picohttpparser \
|
||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/libuv/include \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt
|
-Ideps/xopt \
|
||||||
|
-Wdouble-promotion \
|
||||||
|
-Werror
|
||||||
|
|
||||||
BASE64C_SOURCES = deps/base64c/src/base64c.c
|
BLOWFISH_SOURCES := \
|
||||||
BASE64C_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(BASE64C_SOURCES))
|
|
||||||
$(BASE64C_OBJS): CFLAGS += \
|
|
||||||
-Wno-sign-compare
|
|
||||||
|
|
||||||
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,91 +163,336 @@ 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_android := \
|
||||||
deps/libuv/src/uv-data-getter-setters.c \
|
deps/libuv/src/unix/pthread-fixes.c \
|
||||||
deps/libuv/src/version.c
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
UV_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(UV_SOURCES))
|
UV_SOURCES_win := \
|
||||||
|
deps/libuv/src/win/async.c \
|
||||||
|
deps/libuv/src/win/core.c \
|
||||||
|
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/codecs.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_DEFAULT_MEMSTATUS=0 \
|
||||||
|
-DSQLITE_DQS=0 \
|
||||||
|
-DSQLITE_ENABLE_MEMSYS5 \
|
||||||
|
-DSQLITE_ENABLE_FTS5 \
|
||||||
-DSQLITE_ENABLE_JSON1 \
|
-DSQLITE_ENABLE_JSON1 \
|
||||||
-DSQLITE_MAX_LENGTH=5242880 \
|
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
|
||||||
-DSQLITE_MAX_COLUMN=100 \
|
|
||||||
-DSQLITE_MAX_EXPR_DEPTH=20 \
|
|
||||||
-DSQLITE_MAX_COMPOUND_SELECT=3 \
|
|
||||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
|
||||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
|
||||||
-DSQLITE_MAX_ATTACHED=0 \
|
-DSQLITE_MAX_ATTACHED=0 \
|
||||||
|
-DSQLITE_MAX_COLUMN=100 \
|
||||||
|
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||||
|
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||||
|
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||||
|
-DSQLITE_MAX_LENGTH=5242880 \
|
||||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||||
|
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||||
|
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||||
|
-DSQLITE_OMIT_DEPRECATED \
|
||||||
|
-DSQLITE_OMIT_DESERIALIZE \
|
||||||
|
-DSQLITE_OMIT_LOAD_EXTENSION \
|
||||||
|
-DSQLITE_OMIT_TCL_VARIABLE \
|
||||||
|
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||||
-DSQLITE_SECURE_DELETE \
|
-DSQLITE_SECURE_DELETE \
|
||||||
-Wno-implicit-fallthrough
|
-DSQLITE_THREADSAFE=0 \
|
||||||
|
-DSQLITE_UNTESTABLE \
|
||||||
|
-DSQLITE_USE_ALLOCA \
|
||||||
|
-DHAVE_ISNAN \
|
||||||
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-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
|
||||||
|
$(XOPT_OBJS): CFLAGS += \
|
||||||
|
-Wno-implicit-const-int-float-conversion
|
||||||
|
|
||||||
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.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)\" \
|
||||||
-DDUMP_LEAKS \
|
-DCONFIG_BIGNUM \
|
||||||
-D_GNU_SOURCE \
|
-D_GNU_SOURCE \
|
||||||
-Wno-sign-compare \
|
-Wno-enum-conversion \
|
||||||
|
-Wno-implicit-const-int-float-conversion \
|
||||||
-Wno-implicit-fallthrough \
|
-Wno-implicit-fallthrough \
|
||||||
-Wno-unused-variable \
|
-Wno-sign-compare \
|
||||||
-Wno-unused-but-set-variable
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-variable
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
PICOHTTPPARSER_SOURCES := \
|
||||||
|
deps/picohttpparser/picohttpparser.c
|
||||||
|
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
||||||
|
|
||||||
|
MINIUNZIP_SOURCES := \
|
||||||
|
deps/zlib/contrib/minizip/unzip.c \
|
||||||
|
deps/zlib/contrib/minizip/ioapi.c \
|
||||||
|
deps/zlib/adler32.c \
|
||||||
|
deps/zlib/crc32.c \
|
||||||
|
deps/zlib/inffast.c \
|
||||||
|
deps/zlib/inflate.c \
|
||||||
|
deps/zlib/inftrees.c \
|
||||||
|
deps/zlib/zutil.c
|
||||||
|
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
|
||||||
|
$(MINIUNZIP_OBJS): CFLAGS += \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Wno-maybe-uninitialized
|
||||||
|
|
||||||
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
|
-lm
|
||||||
|
debug release: LDFLAGS += \
|
||||||
-ldl \
|
-ldl \
|
||||||
-lm \
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
windebug winrelease: LDFLAGS += \
|
||||||
|
-lwsock32 \
|
||||||
|
-lws2_32 \
|
||||||
|
-lkernel32 \
|
||||||
|
-liphlpapi \
|
||||||
|
-luserenv \
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto \
|
-lcrypto \
|
||||||
-lsodium
|
-lws2_32 \
|
||||||
|
-lcrypt32
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
|
||||||
|
-ldl \
|
||||||
|
-llog \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
|
||||||
DEFAULT_TARGET = $(APP_BIN)
|
unix: debug release
|
||||||
all: $(DEFAULT_TARGET)
|
win: windebug winrelease
|
||||||
.PHONY: all
|
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
|
||||||
|
.PHONY: all win unix
|
||||||
|
|
||||||
ALL_APP_OBJS = \
|
ALL_APP_OBJS := \
|
||||||
$(APP_OBJS) \
|
$(APP_OBJS) \
|
||||||
$(BASE64C_OBJS) \
|
|
||||||
$(BLOWFISH_OBJS) \
|
$(BLOWFISH_OBJS) \
|
||||||
$(UV_OBJS) \
|
$(LIBBACKTRACE_OBJS) \
|
||||||
$(SQLITE_OBJS) \
|
$(MINIUNZIP_OBJS) \
|
||||||
|
$(PICOHTTPPARSER_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))
|
||||||
|
@echo [link] $$@
|
||||||
|
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
|
$(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))))
|
||||||
|
|
||||||
|
# Android support.
|
||||||
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [c] $@
|
@echo [aapt2] $@
|
||||||
@$(CC) $(COMMON_CFLAGS) $(CFLAGS) -c $< -o $@
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||||
|
|
||||||
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
|
@echo [javac] $(CLASS_FILES)
|
||||||
|
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo [d8] $@
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
|
PACKAGE_DIRS := \
|
||||||
|
apps/ \
|
||||||
|
core/ \
|
||||||
|
deps/codemirror/ \
|
||||||
|
deps/lit/ \
|
||||||
|
deps/split/ \
|
||||||
|
deps/smoothie/
|
||||||
|
|
||||||
|
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
|
||||||
|
|
||||||
|
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
|
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
|
||||||
|
out/%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||||
|
@echo [aapt] $@
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
|
||||||
|
@cp out/apk/res.apk $@
|
||||||
|
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
|
||||||
|
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
|
||||||
|
|
||||||
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
|
@echo [apksigner] $(notdir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
||||||
|
|
||||||
|
apk: out/TildeFriends-debug.apk
|
||||||
|
.PHONY: apk
|
||||||
|
|
||||||
|
apkgo: out/TildeFriends-debug.apk
|
||||||
|
@adb install $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||||
|
.PHONY: apkgo
|
||||||
|
|
||||||
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/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.
|
||||||
|
4
apps/admin.json
Normal file
4
apps/admin.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🎛"
|
||||||
|
}
|
22
apps/admin/app.js
Normal file
22
apps/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/admin/index.html
Normal file
10
apps/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/admin/lit.min.js
vendored
Normal file
13
apps/admin/lit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
78
apps/admin/script.js
Normal file
78
apps/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);
|
||||||
|
});
|
4
apps/api.json
Normal file
4
apps/api.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📜"
|
||||||
|
}
|
10
apps/api/app.js
Normal file
10
apps/api/app.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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(globalThis), null, 2)}</pre>`);
|
4
apps/apps.json
Normal file
4
apps/apps.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💻"
|
||||||
|
}
|
77
apps/apps/app.js
Normal file
77
apps/apps/app.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
async function fetch_info(apps) {
|
||||||
|
let result = {};
|
||||||
|
for (let [key, value] of Object.entries(apps)) {
|
||||||
|
let blob = await ssb.blobGet(value);
|
||||||
|
blob = blob ? utf8Decode(blob) : '{}';
|
||||||
|
result[key] = JSON.parse(blob);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
var apps = await fetch_info(await core.apps());
|
||||||
|
var core_apps = await fetch_info(await core.apps('core'));
|
||||||
|
var doc = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 64px);
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
height: 96px;
|
||||||
|
width: 64px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.app > a {
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 64px;
|
||||||
|
text-overflow: ellipsis ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="background: #888">
|
||||||
|
<h1 id="apps_title">Apps</h1>
|
||||||
|
<div id="apps" class="container"></div>
|
||||||
|
<h1>Core Apps</h1>
|
||||||
|
<div id="core_apps" class="container"></div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
function populate_apps(id, name, apps) {
|
||||||
|
var list = document.getElementById(id);
|
||||||
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
let div = list.appendChild(document.createElement('div'));
|
||||||
|
div.classList.add('app');
|
||||||
|
|
||||||
|
let icon_a = document.createElement('a');
|
||||||
|
let icon = document.createElement('div');
|
||||||
|
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||||
|
icon.style.fontSize = 'xxx-large';
|
||||||
|
icon_a.appendChild(icon);
|
||||||
|
icon_a.href = '/~' + name + '/' + app + '/';
|
||||||
|
icon_a.target = '_top';
|
||||||
|
div.appendChild(icon_a);
|
||||||
|
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.appendChild(document.createTextNode(app));
|
||||||
|
a.href = '/~' + name + '/' + app + '/';
|
||||||
|
a.target = '_top';
|
||||||
|
div.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
||||||
|
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();
|
4
apps/appstore.json
Normal file
4
apps/appstore.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🛍"
|
||||||
|
}
|
55
apps/appstore/app.js
Normal file
55
apps/appstore/app.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
async function get_apps() {
|
||||||
|
let results = {};
|
||||||
|
await ssb.sqlStream(`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages_fts('"application/tildefriends"')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
ORDER BY timestamp
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function(row) {
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
for (let mention of content.mentions) {
|
||||||
|
if (mention?.type === 'application/tildefriends') {
|
||||||
|
results[JSON.stringify([row.author, mention.name])] = {
|
||||||
|
message: row,
|
||||||
|
blob: mention.link,
|
||||||
|
name: mention.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_app(app) {
|
||||||
|
return `
|
||||||
|
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
|
||||||
|
<a href="/~cory/ssb/#${app.message.author}">@</a>
|
||||||
|
<a href="/~cory/ssb/#${app.message.id}">%</a>
|
||||||
|
<a href="/${app.blob}/">${app.name}</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let apps = await get_apps();
|
||||||
|
app.setDocument(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top">
|
||||||
|
<style>
|
||||||
|
a:link { color: #bbf; }
|
||||||
|
a:visited { color: #ddd; }
|
||||||
|
a:hover { color: #ddf; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff">
|
||||||
|
<h1>${apps.length} apps</h1>
|
||||||
|
${apps.map(render_app).join('\n')}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
@ -1 +0,0 @@
|
|||||||
{"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"}}
|
|
File diff suppressed because one or more lines are too long
@ -1,11 +0,0 @@
|
|||||||
# Tilde Friends Documentation
|
|
||||||
|
|
||||||
Tilde Friends is a participating member of a greater social
|
|
||||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
|
||||||
augmenting it with a way to safely and securely write, share,
|
|
||||||
and run code.
|
|
||||||
|
|
||||||
- [Purpose](#purpose)
|
|
||||||
- [Structure](#structure)
|
|
||||||
- [Guide](#guide)
|
|
||||||
- [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.
|
|
@ -1,48 +0,0 @@
|
|||||||
# Tilde Friends TODO
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
## MVP
|
|
||||||
- release
|
|
||||||
- blog
|
|
||||||
- update COPYING
|
|
||||||
- update README
|
|
||||||
- auto-populate data on initial launch
|
|
||||||
- audit + document API exposed to apps
|
|
||||||
- ssb core
|
|
||||||
- good refresh
|
|
||||||
- disconnect all current connections and reset reconnect timers?
|
|
||||||
- reload the page
|
|
||||||
- live updates
|
|
||||||
- createHistoryStream for every account followed from local accounts
|
|
||||||
- apps
|
|
||||||
- app messages
|
|
||||||
- installable apps
|
|
||||||
- web interface
|
|
||||||
- live updates
|
|
||||||
- strip out unnecessary things?
|
|
||||||
- more raw views until it's more functional?
|
|
||||||
|
|
||||||
## Done
|
|
||||||
- likely classes of script errors
|
|
||||||
- tf core
|
|
||||||
- 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 +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
4
apps/db.json
Normal file
4
apps/db.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "💽"
|
||||||
|
}
|
70
apps/db/app.js
Normal file
70
apps/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();
|
4
apps/docs.json
Normal file
4
apps/docs.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📚"
|
||||||
|
}
|
29
apps/docs/app.js
Normal file
29
apps/docs/app.js
Normal file
File diff suppressed because one or more lines are too long
12
apps/docs/index.md
Normal file
12
apps/docs/index.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Tilde Friends Documentation
|
||||||
|
|
||||||
|
Tilde Friends is a participating member of a greater social
|
||||||
|
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
||||||
|
adding a way to safely and securely write, share,
|
||||||
|
and run code in the form of server-side web applications.
|
||||||
|
|
||||||
|
- [Tilde Friends Vision](#vision)
|
||||||
|
- [Secure Scuttlebutt from Scratch](#ssb)
|
||||||
|
- [Structure](#structure)
|
||||||
|
- [Guide](#guide)
|
||||||
|
- [TODO](#todo)
|
41
apps/docs/ssb.md
Normal file
41
apps/docs/ssb.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
There also an undocumented "new" style of discovery message.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
... `ebt.replicate` or `createHistoryStream` ...
|
||||||
|
|
||||||
|
## Rooms
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## References
|
||||||
|
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
|
||||||
|
* [https://dev.planetary.social/](https://dev.planetary.social/)
|
||||||
|
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)
|
@ -21,7 +21,7 @@ In combines the following key components:
|
|||||||
are mediated through the core process.
|
are mediated through the core process.
|
||||||
|
|
||||||
When run with no arguments, it starts a web server on
|
When run with no arguments, it starts a web server on
|
||||||
[http://localhost:12345/](http://localhost:12345/) and an SSB server.
|
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
|
||||||
|
|
||||||
## Web Interface
|
## Web Interface
|
||||||
The Tilde Friends web server provides access to Tilde Friends applications,
|
The Tilde Friends web server provides access to Tilde Friends applications,
|
||||||
@ -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
|
63
apps/docs/todo.md
Normal file
63
apps/docs/todo.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Tilde Friends TODO
|
||||||
|
[Back to index](#index)
|
||||||
|
|
||||||
|
## MVP3
|
||||||
|
- Sync status (problem feeds, messages/seconds stats, ...)
|
||||||
|
- app: wiki
|
||||||
|
- app: public blog
|
||||||
|
- Content-Disposition: download
|
||||||
|
- remove SSB credentials
|
||||||
|
- export SSB credentials
|
||||||
|
- initial: better empty news screen
|
||||||
|
- initial: remembered wrong user across login/logout
|
||||||
|
- initial: bad experience when following nobody
|
||||||
|
- make a cool independent app
|
||||||
|
- indicate when workspace differs from installed
|
||||||
|
- / => Something good.
|
||||||
|
- update docs
|
||||||
|
- audit + document API exposed to apps
|
||||||
|
- sqlStream => sqlExec or something
|
||||||
|
- fix weird HTTP warnings
|
||||||
|
- ssb from child process?
|
||||||
|
- channels
|
||||||
|
- placeholder/missing images
|
||||||
|
- no denial of service
|
||||||
|
- package standalone executable
|
||||||
|
- editor without app iframe
|
||||||
|
- sequence_before_author -> flags
|
||||||
|
- linkify ssb: links
|
||||||
|
- perfect rooms support
|
||||||
|
- connections 2.0
|
||||||
|
- make a better connections API
|
||||||
|
|
||||||
|
## Maybe Done
|
||||||
|
- blob_wants 2.0
|
||||||
|
- image downsample
|
||||||
|
- app: todo
|
||||||
|
- app: build archive
|
||||||
|
- 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
|
||||||
|
- update LICENSE
|
||||||
|
- logging to browser
|
62
apps/docs/vision.md
Normal file
62
apps/docs/vision.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Tilde Friends Vision
|
||||||
|
[Back to index](#index)
|
||||||
|
|
||||||
|
Tilde Friends is a tool for making and sharing.
|
||||||
|
|
||||||
|
It is both a peer-to-peer social network client, participating in Secure
|
||||||
|
Scuttlebutt, and an environment for creating and running web applications.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
This is a thing that I wanted to exist and wanted to work on. No other reason.
|
||||||
|
There is not a business model. I believe it is interesting and unique.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
1. Make it **easy and fun** to run all sorts of web applications.
|
||||||
|
|
||||||
|
2. Provide **security** that is easy to understand and protects your data.
|
||||||
|
|
||||||
|
3. Make **creating and sharing** web applications accessible to anyone with a
|
||||||
|
browser.
|
||||||
|
|
||||||
|
## Ways to Use Tilde Friends
|
||||||
|
1. **Social Network User**: This is a social network first. You are just here,
|
||||||
|
because your friends are. Or you like how we limit your message length or
|
||||||
|
short videos or whatever the trend is. If you are ambitious, you click links
|
||||||
|
and see interactive experiences (apps) that you wouldn't see elsewhere.
|
||||||
|
|
||||||
|
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
|
||||||
|
lists, wiki pages, ..., and you interact with them as though they were
|
||||||
|
cloud-hosted by a megacorporation. They just work, and you don't think twice.
|
||||||
|
|
||||||
|
3. **Group leader**: You host or use a small public instance, installing apps for
|
||||||
|
a group of friends to use as web visitors.
|
||||||
|
|
||||||
|
4. **Developer**: You like to write code and make or improve apps for fun or to
|
||||||
|
solve problems. When you encounter a Tilde Friends app on a strange server,
|
||||||
|
you know you can trivially modify it or download it to your own instance.
|
||||||
|
|
||||||
|
## Future Goals / Endgame
|
||||||
|
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
|
||||||
|
the web interface publicly, but you can sync, install and edit apps, and
|
||||||
|
otherwise get the full experience from a tiny touch screen.
|
||||||
|
|
||||||
|
2. The universal application runtime. The web browser is the universal
|
||||||
|
platform, but even for the simplest application that you might want to host
|
||||||
|
for your friends, cloud hosting, containers, and complicated dependencies might
|
||||||
|
all enter the mix. Tilde Friends, though it is yet another thing to host,
|
||||||
|
includes everything you need out of the box to run a vast variety of interesting
|
||||||
|
apps.
|
||||||
|
|
||||||
|
Tilde Friends will be built out, gradually providing safe access to host
|
||||||
|
resources and client resources the same way web browsers extended access to
|
||||||
|
resources like GPU, persistent storage, cameras, ... over the years.
|
||||||
|
|
||||||
|
Not much effort has been put forward yet to having a robust, long-lasting API,
|
||||||
|
but since the client side longevity is already handled by web browsers, it
|
||||||
|
seems possible that the server-side API can be managed in a similar way.
|
||||||
|
|
||||||
|
3. An awesome development environment. Right now it runs JavaScript from the
|
||||||
|
first embeddable text editor I could poorly configure enough to edit code,
|
||||||
|
but it could incorporate a debugger, source control integration a la ssb-git,
|
||||||
|
merge tools, and transpiling from all sorts of different languages.
|
4
apps/follow.json
Normal file
4
apps/follow.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "➡️"
|
||||||
|
}
|
157
apps/follow/app.js
Normal file
157
apps/follow/app.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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.sqlAsync(
|
||||||
|
"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.sqlAsync(
|
||||||
|
"SELECT "+
|
||||||
|
" sequence, "+
|
||||||
|
" content "+
|
||||||
|
"FROM messages "+
|
||||||
|
"WHERE "+
|
||||||
|
" author = ?1 AND "+
|
||||||
|
" sequence > ?2 AND "+
|
||||||
|
" json_extract(content, '$.type') = 'about' AND "+
|
||||||
|
" json_extract(content, '$.about') = ?1 "+
|
||||||
|
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
||||||
|
"ORDER BY sequence",
|
||||||
|
[id, f.sequence],
|
||||||
|
function(row) {
|
||||||
|
f.sequence = row.sequence;
|
||||||
|
if (row.content) {
|
||||||
|
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.sqlAsync(
|
||||||
|
"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();
|
4
apps/ssb.json
Normal file
4
apps/ssb.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🐌"
|
||||||
|
}
|
99
apps/ssb/app.js
Normal file
99
apps/ssb/app.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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 getStoredConnections() {
|
||||||
|
return ssb.storedConnections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function forgetStoredConnection(connection) {
|
||||||
|
return ssb.forgetStoredConnection(connection);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createTunnel(portal, target) {
|
||||||
|
return ssb.createTunnel(portal, target);
|
||||||
|
});
|
||||||
|
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.sqlAsync(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/ssb/commonmark-hashtag.js
Normal file
90
apps/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)#[\\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/ssb/commonmark-linkify.js
Normal file
91
apps/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/ssb/commonmark.min.js
vendored
Normal file
1
apps/ssb/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
109
apps/ssb/emojis.js
Normal file
109
apps/ssb/emojis.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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 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.minWidth = 'min(16em, 90vw)';
|
||||||
|
div.style.width = 'min(16em, 90vw)';
|
||||||
|
div.style.maxWidth = 'min(16em, 90vw)';
|
||||||
|
div.style.maxHeight = '16em';
|
||||||
|
div.style.overflow = 'scroll';
|
||||||
|
div.style.fontWeight = 'bold';
|
||||||
|
div.style.fontSize = 'xx-large';
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.style.display = 'block';
|
||||||
|
input.style.boxSizing = 'border-box';
|
||||||
|
input.style.width = '100%';
|
||||||
|
input.style.margin = '0';
|
||||||
|
input.style.position = 'relative';
|
||||||
|
div.appendChild(input);
|
||||||
|
let list = document.createElement('div');
|
||||||
|
div.appendChild(list);
|
||||||
|
div.addEventListener('mousedown', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
console.log('emoji cleanup');
|
||||||
|
div.parentElement.removeChild(div);
|
||||||
|
window.removeEventListener('keydown', key_down);
|
||||||
|
console.log('removing click');
|
||||||
|
document.body.removeEventListener('mousedown', cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
function key_down(event) {
|
||||||
|
if (event.key == 'Escape') {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
while (list.firstChild) {
|
||||||
|
list.removeChild(list.firstChild);
|
||||||
|
}
|
||||||
|
let search = input.value;
|
||||||
|
let any_at_all = false;
|
||||||
|
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.display = 'inline-block';
|
||||||
|
emoji.style.overflow = 'hidden';
|
||||||
|
emoji.style.cursor = 'pointer';
|
||||||
|
emoji.onclick = function() {
|
||||||
|
callback(entry);
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
emoji.title = entry.name;
|
||||||
|
emoji.appendChild(document.createTextNode(entry.emoji));
|
||||||
|
list.appendChild(emoji);
|
||||||
|
any = true;
|
||||||
|
any_at_all = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
list.removeChild(header);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!any_at_all) {
|
||||||
|
list.appendChild(document.createTextNode('No matches found.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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%)';
|
||||||
|
input.focus();
|
||||||
|
console.log('adding click');
|
||||||
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
|
window.addEventListener('keydown', key_down);
|
||||||
|
});
|
||||||
|
}
|
15115
apps/ssb/emojis.json
Normal file
15115
apps/ssb/emojis.json
Normal file
File diff suppressed because it is too large
Load Diff
21
apps/ssb/index.html
Normal file
21
apps/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>
|
126
apps/ssb/lit-all.min.js
vendored
Normal file
126
apps/ssb/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/ssb/lit-all.min.js.map
Normal file
1
apps/ssb/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
13
apps/ssb/script.js
Normal file
13
apps/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';
|
324
apps/ssb/tf-app.js
Normal file
324
apps/ssb/tf-app.js
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
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 all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
|
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
||||||
|
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_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;
|
||||||
|
let store = JSON.stringify(cache);
|
||||||
|
/* 2023-02-20: Exceeding message size. */
|
||||||
|
//if (store.length < 512 * 1024) {
|
||||||
|
await tfrpc.rpc.databaseSet('following', store);
|
||||||
|
//}
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
${contents}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-app', TfElement);
|
375
apps/ssb/tf-compose.js
Normal file
375
apps/ssb/tf-compose.js
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} 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},
|
||||||
|
apps: {type: Object},
|
||||||
|
drafts: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.users = {};
|
||||||
|
this.root = undefined;
|
||||||
|
this.branch = undefined;
|
||||||
|
this.apps = undefined;
|
||||||
|
this.drafts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
process_text(text) {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
/* Update mentions. */
|
||||||
|
let draft = this.get_draft();
|
||||||
|
let updated = false;
|
||||||
|
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 (!draft.mentions) {
|
||||||
|
draft.mentions = {};
|
||||||
|
}
|
||||||
|
if (!draft.mentions[link]) {
|
||||||
|
draft.mentions[link] = {
|
||||||
|
link: link,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
return tfutils.markdown(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input(event) {
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
|
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
||||||
|
if (content_warning && content_warning_preview) {
|
||||||
|
content_warning_preview.innerText = content_warning.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(draft) {
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: this.branch,
|
||||||
|
draft: draft
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
change() {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.text = this.renderRoot.getElementById('edit')?.value;
|
||||||
|
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
||||||
|
this.notify(draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
convert_to_format(buffer, type, mime_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(mime_type);
|
||||||
|
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 draft = this.get_draft();
|
||||||
|
let self = this;
|
||||||
|
let buffer = await file.arrayBuffer();
|
||||||
|
let type = file.type;
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
let best_buffer;
|
||||||
|
let best_type;
|
||||||
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||||
|
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
||||||
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||||
|
best_buffer = test_buffer;
|
||||||
|
best_type = format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer = best_buffer;
|
||||||
|
type = best_type;
|
||||||
|
} else {
|
||||||
|
buffer = Array.from(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
let id = await tfrpc.rpc.store_blob(buffer);
|
||||||
|
let name = type.split('/')[0] + ':' + file.name;
|
||||||
|
if (!draft.mentions) {
|
||||||
|
draft.mentions = {};
|
||||||
|
}
|
||||||
|
draft.mentions[id] = {
|
||||||
|
link: id,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
size: buffer.length ?? buffer.byteLength,
|
||||||
|
};
|
||||||
|
let edit = self.renderRoot.getElementById('edit');
|
||||||
|
edit.value += `\n`;
|
||||||
|
self.change();
|
||||||
|
self.input();
|
||||||
|
} 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 draft = this.get_draft();
|
||||||
|
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(draft.mentions || {}).length) {
|
||||||
|
message.mentions = Object.values(draft.mentions);
|
||||||
|
}
|
||||||
|
if (draft.content_warning !== undefined) {
|
||||||
|
message.contentWarning = draft.content_warning;
|
||||||
|
}
|
||||||
|
console.log('Would post:', message);
|
||||||
|
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
|
edit.value = '';
|
||||||
|
self.change();
|
||||||
|
self.notify(undefined);
|
||||||
|
self.requestUpdate();
|
||||||
|
}).catch(function(error) {
|
||||||
|
alert(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discard() {
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
edit.value = '';
|
||||||
|
this.change();
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
preview.innerHTML = '';
|
||||||
|
this.notify(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
super.updated();
|
||||||
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
|
if (this.last_updated_text !== edit.value) {
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
|
this.last_updated_text = edit.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_mention(id) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
delete draft.mentions[id];
|
||||||
|
this.notify(draft);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
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],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let draft = self.get_draft();
|
||||||
|
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
||||||
|
self.requestUpdate();
|
||||||
|
self.notify(draft);
|
||||||
|
self.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() {
|
||||||
|
let self = this;
|
||||||
|
async function attach_app() {
|
||||||
|
self.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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_content_warning(value) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.content_warning = value;
|
||||||
|
this.notify(draft);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_content_warning() {
|
||||||
|
let self = this;
|
||||||
|
let draft = this.get_draft();
|
||||||
|
if (draft.content_warning !== undefined) {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
|
||||||
|
<label for="cw">CW</label>
|
||||||
|
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
|
<label for="cw">CW</label>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_draft() {
|
||||||
|
return this.drafts[this.branch || ''] || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
let draft = self.get_draft();
|
||||||
|
let content_warning =
|
||||||
|
draft.content_warning !== undefined ?
|
||||||
|
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
||||||
|
undefined;
|
||||||
|
let result = html`
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
${content_warning}
|
||||||
|
<div id="preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
||||||
|
${this.render_content_warning()}
|
||||||
|
${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);
|
37
apps/ssb/tf-id-picker.js
Normal file
37
apps/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);
|
443
apps/ssb/tf-message.js
Normal file
443
apps/ssb/tf-message.js
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
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},
|
||||||
|
drafts: {type: Object},
|
||||||
|
raw: {type: Boolean},
|
||||||
|
blog_data: {type: String},
|
||||||
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.message = {};
|
||||||
|
this.users = {};
|
||||||
|
this.drafts = {};
|
||||||
|
this.raw = false;
|
||||||
|
this.expanded = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
show_reply() {
|
||||||
|
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
|
||||||
|
this.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
discard_reply() {
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
||||||
|
let next = event.srcElement.nextSibling;
|
||||||
|
if (next.style.display == 'block') {
|
||||||
|
next.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
next.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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; max-width: 128px">
|
||||||
|
<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 => 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
total_child_messages(message) {
|
||||||
|
if (!message.child_messages) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let total = message.child_messages.length;
|
||||||
|
for (let m of message.child_messages)
|
||||||
|
{
|
||||||
|
total += this.total_child_messages(m);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_expanded(expanded, tag) {
|
||||||
|
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_expanded(tag) {
|
||||||
|
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_children() {
|
||||||
|
let self = this;
|
||||||
|
if (this.message.child_messages?.length) {
|
||||||
|
if (!this.expanded[this.message.id]) {
|
||||||
|
return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
|
||||||
|
} else {
|
||||||
|
return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></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; overflow-wrap: anywhere">
|
||||||
|
<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?.type === 'contact_group') {
|
||||||
|
return html`
|
||||||
|
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
||||||
|
${this.message.messages.map(x =>
|
||||||
|
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
} else 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} .drafts=${this.drafts} .expanded=${this.expanded}></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=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (content.description !== undefined) {
|
||||||
|
description = html`
|
||||||
|
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||||
|
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
let update = content.about == this.message.author ?
|
||||||
|
html`<div style="font-weight: bold">Updated profile.</div>` :
|
||||||
|
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
||||||
|
return small_frame(html`
|
||||||
|
${update}
|
||||||
|
${name}
|
||||||
|
${image}
|
||||||
|
${description}
|
||||||
|
`);
|
||||||
|
} else if (content.type == 'contact') {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
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.drafts[this.message?.id] !== undefined) ? html`
|
||||||
|
<tf-compose
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${this.message.content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-discard=${this.discard_reply}></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 class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
||||||
|
`;
|
||||||
|
let content_html =
|
||||||
|
html`
|
||||||
|
<div @click=${this.body_click}>${body}</div>
|
||||||
|
${this.render_mentions()}
|
||||||
|
`;
|
||||||
|
let payload =
|
||||||
|
content.contentWarning ?
|
||||||
|
self.expanded[(this.message.id || '') + ':cw'] ?
|
||||||
|
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;
|
||||||
|
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
||||||
|
self.blog_data = data;
|
||||||
|
});
|
||||||
|
let payload =
|
||||||
|
this.expanded[(this.message.id || '') + ':blog'] ?
|
||||||
|
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.toggle_expanded(':blog')}>
|
||||||
|
<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`
|
||||||
|
<style>
|
||||||
|
span {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<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);
|
183
apps/ssb/tf-news.js
Normal file
183
apps/ssb/tf-news.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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},
|
||||||
|
drafts: {type: Object},
|
||||||
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.messages = [];
|
||||||
|
this.following = [];
|
||||||
|
this.drafts = {};
|
||||||
|
this.expanded = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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.votes = [];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
group_following(messages) {
|
||||||
|
let result = [];
|
||||||
|
let group = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
if (message?.content?.type === 'contact') {
|
||||||
|
group.push(message);
|
||||||
|
} else {
|
||||||
|
if (group.length > 0) {
|
||||||
|
result.push({
|
||||||
|
type: 'contact_group',
|
||||||
|
messages: group,
|
||||||
|
});
|
||||||
|
group = [];
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
load_and_render(messages) {
|
||||||
|
let messages_by_id = this.process_messages(messages);
|
||||||
|
let final_messages = this.group_following(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} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.load_and_render(this.messages || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-news', TfNewsElement);
|
180
apps/ssb/tf-profile.js
Normal file
180
apps/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);
|
48
apps/ssb/tf-styles.js
Normal file
48
apps/ssb/tf-styles.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content_warning {
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 1em;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.img_caption {
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.img_caption::after {
|
||||||
|
content: ' ±';
|
||||||
|
}
|
||||||
|
`;
|
122
apps/ssb/tf-tab-connections.js
Normal file
122
apps/ssb/tf-tab-connections.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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},
|
||||||
|
stored_connections: {type: Array},
|
||||||
|
users: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.broadcasts = [];
|
||||||
|
this.identities = [];
|
||||||
|
this.connections = [];
|
||||||
|
this.stored_connections = [];
|
||||||
|
this.users = {};
|
||||||
|
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||||
|
self.identities = identities || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getStoredConnections().then(function(connections) {
|
||||||
|
self.stored_connections = connections || [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return tfrpc.rpc.createTunnel(portal, 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async forget_stored_connection(connection) {
|
||||||
|
await tfrpc.rpc.forgetStoredConnection(connection);
|
||||||
|
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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>Stored Connections (WIP)</h2>
|
||||||
|
<ul>
|
||||||
|
${this.stored_connections.map(x => html`
|
||||||
|
<li>
|
||||||
|
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
|
||||||
|
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
|
||||||
|
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.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-tab-connections', TfTabConnectionsElement);
|
211
apps/ssb/tf-tab-news.js
Normal file
211
apps/ssb/tf-tab-news.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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},
|
||||||
|
drafts: {type: Object},
|
||||||
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.hash = '#';
|
||||||
|
this.following = [];
|
||||||
|
this.drafts = {};
|
||||||
|
this.expanded = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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} .drafts=${this.drafts} .expanded=${this.expanded}></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},
|
||||||
|
drafts: {type: Object},
|
||||||
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.hash = '#';
|
||||||
|
this.unread = [];
|
||||||
|
this.following = [];
|
||||||
|
this.cache = {};
|
||||||
|
this.drafts = {};
|
||||||
|
this.expanded = {};
|
||||||
|
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
|
||||||
|
self.drafts = JSON.parse(d || '{}');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
draft(event) {
|
||||||
|
let id = event.detail.id || '';
|
||||||
|
let previous = this.drafts[id];
|
||||||
|
if (event.detail.draft !== undefined) {
|
||||||
|
this.drafts[id] = event.detail.draft;
|
||||||
|
} else {
|
||||||
|
delete this.drafts[id];
|
||||||
|
}
|
||||||
|
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
||||||
|
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
||||||
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
|
}
|
||||||
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
||||||
|
${profile}
|
||||||
|
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
||||||
|
customElements.define('tf-tab-news', TfTabNewsElement);
|
73
apps/ssb/tf-tab-search.js
Normal file
73
apps/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);
|
44
apps/ssb/tf-user.js
Normal file
44
apps/ssb/tf-user.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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() {
|
||||||
|
let name = this.users?.[this.id]?.name;
|
||||||
|
name = name !== undefined ?
|
||||||
|
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
||||||
|
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||||
|
|
||||||
|
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}">
|
||||||
|
${name}
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
<div style="display: inline-block; font-weight: bold">
|
||||||
|
${name}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-user', TfUserElement);
|
94
apps/ssb/tf-utils.js
Normal file
94
apps/ssb/tf-utils.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
|
|
||||||
|
function image(node, entering) {
|
||||||
|
if (node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</video>');
|
||||||
|
}
|
||||||
|
} else if (node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</audio>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entering) {
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
||||||
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
|
this.lit('<img src="" alt="');
|
||||||
|
} else {
|
||||||
|
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
if (node.title) {
|
||||||
|
this.lit('" title="' + this.esc(node.title));
|
||||||
|
}
|
||||||
|
this.lit('" />');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
writer.image = image;
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
parsed = linkify.transform(parsed);
|
||||||
|
parsed = hashtagify.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/ssb/tribute.css
Normal file
32
apps/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/ssb/tribute.esm.js
Normal file
1797
apps/ssb/tribute.esm.js
Normal file
File diff suppressed because it is too large
Load Diff
2
apps/ssb/update.sh
Normal file
2
apps/ssb/update.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
wget https://cdn.jsdelivr.net/gh/lit/dist@2.7.2/all/lit-all.min.js -O lit-all.min.js
|
||||||
|
wget https://cdn.jsdelivr.net/gh/lit/dist@2.7.2/all/lit-all.min.js.map -O lit-all.min.js.map
|
4
apps/todo.json
Normal file
4
apps/todo.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "☑️"
|
||||||
|
}
|
82
apps/todo/app.js
Normal file
82
apps/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/todo/index.html
Normal file
11
apps/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/todo/lit-core.min.js
vendored
Normal file
29
apps/todo/lit-core.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
187
apps/todo/script.js
Normal file
187
apps/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)} style="cursor: pointer">❎</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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || []).filter(item => !item.x).map(x => self.render_item(x))}
|
||||||
|
${(this.items || []).filter(item => item.x).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);
|
145
core/app.js
145
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;
|
||||||
@ -13,34 +23,62 @@ App.prototype.readOutput = function(callback) {
|
|||||||
App.prototype.makeFunction = function(api) {
|
App.prototype.makeFunction = function(api) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let result = function() {
|
let result = function() {
|
||||||
let message = {action: api[0]};
|
let id = g_next_id++;
|
||||||
for (let i = 1; i < api.length; i++) {
|
while (!id || g_calls[id]) {
|
||||||
message[api[i]] = arguments[i - 1];
|
id = g_next_id++;
|
||||||
}
|
}
|
||||||
|
let promise = new Promise(function(resolve, reject) {
|
||||||
|
g_calls[id] = {resolve: resolve, reject: reject};
|
||||||
|
});
|
||||||
|
let message = {
|
||||||
|
message: 'tfrpc',
|
||||||
|
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>
|
||||||
|
283
core/auth.js
283
core/auth.js
@ -1,44 +1,72 @@
|
|||||||
"use strict";
|
import * as core from './core.js';
|
||||||
|
import * as form from './form.js';
|
||||||
|
|
||||||
var gTokens = {};
|
let gDatabase = new Database("auth");
|
||||||
|
|
||||||
var form = require('form');
|
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
||||||
var http = require('http');
|
|
||||||
|
|
||||||
var gDatabase = new Database("auth");
|
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) {
|
||||||
@ -46,48 +74,76 @@ function verifyPassword(password, hash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hashPassword(password) {
|
function hashPassword(password) {
|
||||||
var salt = bCrypt.gensalt(12);
|
let salt = bCrypt.gensalt(12);
|
||||||
return bCrypt.hashpw(password, salt);
|
return bCrypt.hashpw(password, salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 session = getCookies(request.headers).session;
|
let cookies = {};
|
||||||
if (request.uri == "/login") {
|
|
||||||
var sessionIsNew = false;
|
|
||||||
var loginError;
|
|
||||||
|
|
||||||
var formData = form.decodeForm(request.query);
|
if (headers.cookie) {
|
||||||
|
let parts = headers.cookie.split(/,|;/);
|
||||||
|
for (let i in parts) {
|
||||||
|
let equals = parts[i].indexOf("=");
|
||||||
|
let name = parts[i].substring(0, equals).trim();
|
||||||
|
let value = parts[i].substring(equals + 1).trim();
|
||||||
|
cookies[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(request, response) {
|
||||||
|
let session = getCookies(request.headers).session;
|
||||||
|
if (request.uri == "/login") {
|
||||||
|
let sessionIsNew = false;
|
||||||
|
let loginError;
|
||||||
|
|
||||||
|
let 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);
|
let 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 +156,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 +166,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";
|
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict`;
|
||||||
var entry = readSession(session);
|
let 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 = "";
|
let html = utf8Decode(data);
|
||||||
|
let 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';
|
let 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=/; ${request.client.tls ? '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"});
|
||||||
@ -169,8 +233,8 @@ function authHandler(request, response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPermissions(session) {
|
function getPermissions(session) {
|
||||||
var permissions;
|
let permissions;
|
||||||
var entry = readSession(session);
|
let entry = readSession(session);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
permissions = getPermissionsForUser(entry.name);
|
permissions = getPermissionsForUser(entry.name);
|
||||||
permissions.authenticated = entry.name !== "guest";
|
permissions.authenticated = entry.name !== "guest";
|
||||||
@ -179,22 +243,29 @@ function getPermissions(session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionsForUser(userName) {
|
function getPermissionsForUser(userName) {
|
||||||
var permissions = {};
|
let 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 (let i in core.globalSettings.permissions[userName]) {
|
||||||
permissions[gGlobalSettings.permissions[userName][i]] = true;
|
permissions[core.globalSettings.permissions[userName][i]] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return permissions;
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function query(headers) {
|
function query(headers) {
|
||||||
var session = getCookies(headers).session;
|
let session = getCookies(headers).session;
|
||||||
var entry;
|
let entry;
|
||||||
if (entry = readSession(session)) {
|
let 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;
|
|
||||||
|
1167
core/client.js
1167
core/client.js
File diff suppressed because it is too large
Load Diff
967
core/core.js
967
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
22
core/form.js
22
core/form.js
@ -1,7 +1,7 @@
|
|||||||
function decode(encoded) {
|
function decode(encoded) {
|
||||||
var result = "";
|
let result = "";
|
||||||
for (var i = 0; i < encoded.length; i++) {
|
for (let i = 0; i < encoded.length; i++) {
|
||||||
var c = encoded[i];
|
let c = encoded[i];
|
||||||
if (c == "+") {
|
if (c == "+") {
|
||||||
result += " ";
|
result += " ";
|
||||||
} else if (c == "%") {
|
} else if (c == "%") {
|
||||||
@ -15,19 +15,19 @@ function decode(encoded) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function decodeForm(encoded, initial) {
|
function decodeForm(encoded, initial) {
|
||||||
var result = initial || {};
|
let result = initial || {};
|
||||||
if (encoded) {
|
if (encoded) {
|
||||||
encoded = encoded.trim();
|
encoded = encoded.trim();
|
||||||
var items = encoded.split('&');
|
let items = encoded.split('&');
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
var item = items[i];
|
let item = items[i];
|
||||||
var equals = item.indexOf('=');
|
let equals = item.indexOf('=');
|
||||||
var key = decode(item.slice(0, equals));
|
let key = decode(item.slice(0, equals));
|
||||||
var value = decode(item.slice(equals + 1));
|
let value = decode(item.slice(equals + 1));
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.decodeForm = decodeForm;
|
export { decodeForm };
|
||||||
|
61
core/http.js
61
core/http.js
@ -1,61 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function parseUrl(url) {
|
|
||||||
// XXX: Hack.
|
|
||||||
var match = url.match(new RegExp("(\\w+)://([^/]+)?(.*)"));
|
|
||||||
return {
|
|
||||||
protocol: match[1],
|
|
||||||
host: match[2],
|
|
||||||
path: match[3],
|
|
||||||
port: match[1] == "http" ? 80 : 443,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseResponse(data) {
|
|
||||||
var firstLine;
|
|
||||||
var headers = {};
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var endLine = data.indexOf("\r\n");
|
|
||||||
var line = data.substring(0, endLine);
|
|
||||||
if (!firstLine) {
|
|
||||||
firstLine = line;
|
|
||||||
} else if (!line.length) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
var colon = line.indexOf(":");
|
|
||||||
headers[line.substring(colon)] = line.substring(colon + 1);
|
|
||||||
}
|
|
||||||
data = data.substring(endLine + 2);
|
|
||||||
}
|
|
||||||
return {body: data};
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(url) {
|
|
||||||
var parsed = parseUrl(url);
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var socket = new Socket();
|
|
||||||
var buffer = "";
|
|
||||||
|
|
||||||
return socket.connect(parsed.host, parsed.port).then(function() {
|
|
||||||
socket.read(function(data) {
|
|
||||||
if (data) {
|
|
||||||
buffer += data;
|
|
||||||
} else {
|
|
||||||
resolve(parseResponse(buffer));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parsed.port == 443) {
|
|
||||||
return socket.startTls();
|
|
||||||
}
|
|
||||||
}).then(function() {
|
|
||||||
socket.write(`GET ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\n\r\n`);
|
|
||||||
socket.shutdown();
|
|
||||||
}).catch(function(error) {
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.get = get;
|
|
530
core/httpd.js
530
core/httpd.js
@ -1,15 +1,22 @@
|
|||||||
"use strict";
|
import * as core from './core.js';
|
||||||
|
|
||||||
var gHandlers = [];
|
let gHandlers = [];
|
||||||
var gSocketHandlers = [];
|
let gSocketHandlers = [];
|
||||||
|
let 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) {
|
||||||
var added = false;
|
let added = false;
|
||||||
for (var i in gHandlers) {
|
for (let i in gHandlers) {
|
||||||
if (gHandlers[i].path == handler.path) {
|
if (gHandlers[i].path == handler.path) {
|
||||||
gHandlers[i] = handler;
|
gHandlers[i] = handler;
|
||||||
added = true;
|
added = true;
|
||||||
@ -40,7 +47,7 @@ function registerSocketHandler(prefix, handler) {
|
|||||||
|
|
||||||
function Request(method, uri, version, headers, body, client) {
|
function Request(method, uri, version, headers, body, client) {
|
||||||
this.method = method;
|
this.method = method;
|
||||||
var index = uri.indexOf("?");
|
let index = uri.indexOf("?");
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
this.uri = uri.slice(0, index);
|
this.uri = uri.slice(0, index);
|
||||||
this.query = uri.slice(index + 1);
|
this.query = uri.slice(index + 1);
|
||||||
@ -48,17 +55,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findHandler(request) {
|
function findHandler(request) {
|
||||||
var matchedHandler = null;
|
let matchedHandler = null;
|
||||||
for (var name in gHandlers) {
|
for (let name in gHandlers) {
|
||||||
var handler = gHandlers[name];
|
let handler = gHandlers[name];
|
||||||
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
||||||
matchedHandler = handler;
|
matchedHandler = handler;
|
||||||
break;
|
break;
|
||||||
@ -68,9 +75,9 @@ function findHandler(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findSocketHandler(request) {
|
function findSocketHandler(request) {
|
||||||
var matchedHandler = null;
|
let matchedHandler = null;
|
||||||
for (var name in gSocketHandlers) {
|
for (let name in gSocketHandlers) {
|
||||||
var handler = gSocketHandlers[name];
|
let handler = gSocketHandlers[name];
|
||||||
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
||||||
matchedHandler = handler;
|
matchedHandler = handler;
|
||||||
break;
|
break;
|
||||||
@ -80,25 +87,28 @@ function findSocketHandler(request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Response(request, client) {
|
function Response(request, client) {
|
||||||
var kStatusText = {
|
let kStatusText = {
|
||||||
101: "Switching Protocols",
|
101: "Switching Protocols",
|
||||||
200: 'OK',
|
200: 'OK',
|
||||||
303: 'See other',
|
303: 'See other',
|
||||||
|
304: 'Not Modified',
|
||||||
|
400: 'Bad Request',
|
||||||
|
401: 'Unauthorized',
|
||||||
403: 'Forbidden',
|
403: 'Forbidden',
|
||||||
404: 'File not found',
|
404: 'File not found',
|
||||||
500: 'Internal server error',
|
500: 'Internal server error',
|
||||||
};
|
};
|
||||||
var _started = false;
|
let _started = false;
|
||||||
var _finished = false;
|
let _finished = false;
|
||||||
var _keepAlive = false;
|
let _keepAlive = false;
|
||||||
var _chunked = false;
|
let _chunked = false;
|
||||||
return {
|
return {
|
||||||
writeHead: function(status) {
|
writeHead: function(status) {
|
||||||
if (_started) {
|
if (_started) {
|
||||||
throw new Error("Response.writeHead called multiple times.");
|
throw new Error("Response.writeHead called multiple times.");
|
||||||
}
|
}
|
||||||
var reason;
|
let reason;
|
||||||
var headers;
|
let headers;
|
||||||
if (arguments.length == 3) {
|
if (arguments.length == 3) {
|
||||||
reason = arguments[1];
|
reason = arguments[1];
|
||||||
headers = arguments[2];
|
headers = arguments[2];
|
||||||
@ -106,11 +116,11 @@ function Response(request, client) {
|
|||||||
reason = kStatusText[status];
|
reason = kStatusText[status];
|
||||||
headers = arguments[1];
|
headers = arguments[1];
|
||||||
}
|
}
|
||||||
var lowerHeaders = {};
|
let lowerHeaders = {};
|
||||||
var requestVersion = request.version.split("/")[1].split(".");
|
let requestVersion = request.version.split("/")[1].split(".");
|
||||||
var responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
|
let responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
|
||||||
var headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
|
let headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
|
||||||
for (var i in headers) {
|
for (let i in headers) {
|
||||||
headerString += i + ": " + headers[i] + "\r\n";
|
headerString += i + ": " + headers[i] + "\r\n";
|
||||||
lowerHeaders[i.toLowerCase()] = headers[i];
|
lowerHeaders[i.toLowerCase()] = headers[i];
|
||||||
}
|
}
|
||||||
@ -127,7 +137,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 +145,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);
|
||||||
},
|
},
|
||||||
@ -162,21 +171,19 @@ function Response(request, client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRequest(request, response) {
|
function handleRequest(request, response) {
|
||||||
var handler = findHandler(request);
|
let handler = findHandler(request);
|
||||||
|
|
||||||
print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");
|
print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");
|
||||||
|
|
||||||
if (handler) {
|
if (handler) {
|
||||||
try {
|
try {
|
||||||
var promise = handler.invoke(request, response);
|
Promise.resolve(handler.invoke(request, response)).catch(function(error) {
|
||||||
if (promise) {
|
response.reportError(error);
|
||||||
promise.catch(function(error) {
|
request.client.close();
|
||||||
response.reportError(error);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
} 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"});
|
||||||
@ -185,11 +192,11 @@ function handleRequest(request, response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWebSocketRequest(request, response, client) {
|
function handleWebSocketRequest(request, response, client) {
|
||||||
var buffer = new Uint8Array(0);
|
let buffer = new Uint8Array(0);
|
||||||
var frame = new Uint8Array(0);
|
let frame;
|
||||||
var frameOpCode = 0x0;
|
let frameOpCode = 0x0;
|
||||||
|
|
||||||
var handler = findSocketHandler(request);
|
let handler = findSocketHandler(request);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
client.close();
|
client.close();
|
||||||
return;
|
return;
|
||||||
@ -200,11 +207,11 @@ 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;
|
let fin = true;
|
||||||
var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
||||||
var mask = false;
|
let mask = false;
|
||||||
if (message.length < 126) {
|
if (message.length < 126) {
|
||||||
packet.push((mask ? (1 << 7) : 0) | message.length);
|
packet.push((mask ? (1 << 7) : 0) | message.length);
|
||||||
} else if (message.length < (1 << 16)) {
|
} else if (message.length < (1 << 16)) {
|
||||||
@ -212,8 +219,8 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
packet.push((message.length >> 8) & 0xff);
|
packet.push((message.length >> 8) & 0xff);
|
||||||
packet.push(message.length & 0xff);
|
packet.push(message.length & 0xff);
|
||||||
} else {
|
} else {
|
||||||
var high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
|
let high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
|
||||||
var low = message.length & 0xffffffff;
|
let low = message.length & 0xffffffff;
|
||||||
packet.push((mask ? (1 << 7) : 0) | 127);
|
packet.push((mask ? (1 << 7) : 0) | 127);
|
||||||
packet.push((high >> 24) & 0xff);
|
packet.push((high >> 24) & 0xff);
|
||||||
packet.push((high >> 16) & 0xff);
|
packet.push((high >> 16) & 0xff);
|
||||||
@ -225,64 +232,74 @@ function handleWebSocketRequest(request, response, client) {
|
|||||||
packet.push(low & 0xff);
|
packet.push(low & 0xff);
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = new Uint8Array(packet.length + message.length);
|
let 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) {
|
||||||
var newBuffer = new Uint8Array(buffer.length + data.length);
|
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||||
newBuffer.set(buffer, 0);
|
newBuffer.set(buffer, 0);
|
||||||
newBuffer.set(data, buffer.length);
|
newBuffer.set(data, buffer.length);
|
||||||
buffer = newBuffer;
|
buffer = newBuffer;
|
||||||
|
|
||||||
while (buffer.length >= 2) {
|
while (buffer.length >= 2) {
|
||||||
var bits0 = buffer[0];
|
let bits0 = buffer[0];
|
||||||
var bits1 = buffer[1];
|
let bits1 = buffer[1];
|
||||||
if (bits1 & (1 << 7) == 0) {
|
if (bits1 & (1 << 7) == 0) {
|
||||||
// Unmasked message.
|
// Unmasked message.
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
var opCode = bits0 & 0xf;
|
let opCode = bits0 & 0xf;
|
||||||
var fin = bits0 & (1 << 7);
|
let fin = bits0 & (1 << 7);
|
||||||
var payloadLength = bits1 & 0x7f;
|
let payloadLength = bits1 & 0x7f;
|
||||||
var maskStart = 2;
|
let maskStart = 2;
|
||||||
|
|
||||||
if (payloadLength == 126) {
|
if (payloadLength == 126) {
|
||||||
payloadLength = 0;
|
payloadLength = 0;
|
||||||
for (var i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
payloadLength <<= 8;
|
payloadLength <<= 8;
|
||||||
payloadLength |= buffer[2 + i];
|
payloadLength |= buffer[2 + i];
|
||||||
}
|
}
|
||||||
maskStart = 4;
|
maskStart = 4;
|
||||||
} else if (payloadLength == 127) {
|
} else if (payloadLength == 127) {
|
||||||
payloadLength = 0;
|
payloadLength = 0;
|
||||||
for (var i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
payloadLength <<= 8;
|
payloadLength <<= 8;
|
||||||
payloadLength |= buffer[2 + i];
|
payloadLength |= buffer[2 + i];
|
||||||
}
|
}
|
||||||
maskStart = 10;
|
maskStart = 10;
|
||||||
}
|
}
|
||||||
var havePayload = buffer.length >= payloadLength + 2 + 4;
|
let havePayload = buffer.length >= payloadLength + 2 + 4;
|
||||||
if (havePayload) {
|
if (havePayload) {
|
||||||
var mask = buffer.slice(maskStart, maskStart + 4);
|
let mask =
|
||||||
var dataStart = maskStart + 4;
|
buffer[maskStart + 0] |
|
||||||
var decoded = new Array(payloadLength);
|
buffer[maskStart + 1] << 8 |
|
||||||
var payload = buffer.slice(dataStart, dataStart + payloadLength);
|
buffer[maskStart + 2] << 16 |
|
||||||
|
buffer[maskStart + 3] << 24;
|
||||||
|
let dataStart = maskStart + 4;
|
||||||
|
let payload = buffer.slice(dataStart, dataStart + payloadLength);
|
||||||
|
let decoded = maskBytes(payload, mask);
|
||||||
buffer = buffer.slice(dataStart + payloadLength);
|
buffer = buffer.slice(dataStart + payloadLength);
|
||||||
for (var i = 0; i < payloadLength; i++) {
|
|
||||||
decoded[i] = payload[i] ^ mask[i % 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
var newBuffer = new Uint8Array(frame.length + decoded.length);
|
if (frame) {
|
||||||
newBuffer.set(frame, 0);
|
let newBuffer = new Uint8Array(frame.length + decoded.length);
|
||||||
newBuffer.set(decoded, frame.length);
|
newBuffer.set(frame, 0);
|
||||||
frame = newBuffer;
|
newBuffer.set(decoded, frame.length);
|
||||||
|
frame = newBuffer;
|
||||||
|
} else {
|
||||||
|
frame = decoded;
|
||||||
|
}
|
||||||
|
|
||||||
if (opCode) {
|
if (opCode) {
|
||||||
frameOpCode = opCode;
|
frameOpCode = opCode;
|
||||||
@ -291,20 +308,24 @@ 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
frame = new Uint8Array(0);
|
frame = undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
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,51 +336,115 @@ 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";
|
let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
let kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||||
var hex = require("sha1").hash(key + kMagic)
|
return base64Encode(sha1Digest(key + kMagic));
|
||||||
var binary = "";
|
}
|
||||||
for (var i = 0; i < hex.length; i += 6) {
|
|
||||||
var characters = hex.substring(i, i + 6);
|
function badRequest(client, reason) {
|
||||||
if (characters.length < 6) {
|
let now = new Date();
|
||||||
characters += "0".repeat(6 - characters.length);
|
let count = 0;
|
||||||
}
|
let old = gBadRequests[client.peerName];
|
||||||
var value = parseInt(characters, 16);
|
if (!old) {
|
||||||
for (var bit = 0; bit < 8 * 3; bit += 6) {
|
gBadRequests[client.peerName] = {
|
||||||
if (i * 8 / 2 + bit >= 8 * hex.length / 2) {
|
expire: new Date(now.getTime() + 1 * 60 * 1000),
|
||||||
binary += kAlphabet.charAt(64);
|
count: 1,
|
||||||
} else {
|
reason: reason,
|
||||||
binary += kAlphabet.charAt((value >> (18 - bit)) & 63);
|
};
|
||||||
}
|
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) {
|
||||||
|
let old = gBadRequests[client.peerName];
|
||||||
|
if (old) {
|
||||||
|
let now = new Date();
|
||||||
|
if (old.expire < now) {
|
||||||
|
delete gBadRequests[client.peerName];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return old.count < 3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return binary;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnection(client) {
|
function handleConnection(client) {
|
||||||
var inputBuffer = new Uint8Array(0);
|
if (!allowRequest(client)) {
|
||||||
var request;
|
print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason);
|
||||||
var headers = {};
|
client.info = 'rejected';
|
||||||
var lineByLine = true;
|
client.close();
|
||||||
var bodyToRead = -1;
|
return;
|
||||||
var body;
|
}
|
||||||
|
|
||||||
|
client.info = 'accepted';
|
||||||
|
let inputBuffer = new Uint8Array(0);
|
||||||
|
let request;
|
||||||
|
let headers = {};
|
||||||
|
let parsing_header = true;
|
||||||
|
let bodyToRead = -1;
|
||||||
|
let body;
|
||||||
|
let requestCount = -1;
|
||||||
|
let readCount = 0;
|
||||||
|
let 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 {
|
||||||
|
let 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);
|
|
||||||
request = undefined;
|
request = undefined;
|
||||||
headers = {};
|
headers = {};
|
||||||
lineByLine = true;
|
parsing_header = true;
|
||||||
bodyToRead = -1;
|
bodyToRead = -1;
|
||||||
body = undefined;
|
body = undefined;
|
||||||
|
client.info = 'reset';
|
||||||
|
resetTimeout(++requestCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
client.info = 'finishing';
|
||||||
var response = new Response(requestObject, client);
|
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
|
let response = new Response(requestObject, client);
|
||||||
try {
|
try {
|
||||||
handleRequest(requestObject, response)
|
handleRequest(requestObject, response)
|
||||||
if (client.isConnected) {
|
if (client.isConnected) {
|
||||||
@ -367,144 +452,175 @@ function handleConnection(client) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
response.reportError(error);
|
response.reportError(error);
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLine(line, length) {
|
|
||||||
if (bodyToRead == -1) {
|
|
||||||
line = new TextDecoder("ASCII").decode(line);
|
|
||||||
if (!request) {
|
|
||||||
request = line.split(' ');
|
|
||||||
return true;
|
|
||||||
} else if (line) {
|
|
||||||
var colon = line.indexOf(':');
|
|
||||||
var key = line.slice(0, colon).trim();
|
|
||||||
var value = line.slice(colon + 1).trim();
|
|
||||||
headers[key.toLowerCase()] = value;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (headers["content-length"] != undefined) {
|
|
||||||
bodyToRead = parseInt(headers["content-length"]);
|
|
||||||
lineByLine = false;
|
|
||||||
body = "";
|
|
||||||
return true;
|
|
||||||
} else if (headers["connection"]
|
|
||||||
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
|
||||||
&& headers["upgrade"]
|
|
||||||
&& headers["upgrade"].toLowerCase() == "websocket") {
|
|
||||||
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
|
||||||
var response = new Response(requestObject, client);
|
|
||||||
handleWebSocketRequest(requestObject, response, client);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
line = new TextDecoder("UTF-8").decode(line);
|
|
||||||
body += line;
|
|
||||||
bodyToRead -= length;
|
|
||||||
if (bodyToRead <= 0) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client.noDelay = true;
|
|
||||||
|
|
||||||
client.onError(function(error) {
|
client.onError(function(error) {
|
||||||
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.read(function(data) {
|
client.read(function(data) {
|
||||||
|
readCount++;
|
||||||
if (data) {
|
if (data) {
|
||||||
var newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
if (bodyToRead != -1 && !isWebsocket) {
|
||||||
|
resetTimeout(requestCount);
|
||||||
|
}
|
||||||
|
let 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);
|
||||||
inputBuffer = newBuffer;
|
inputBuffer = newBuffer;
|
||||||
|
|
||||||
var newLine = '\n'.charCodeAt(0);
|
if (parsing_header)
|
||||||
var carriageReturn = '\r'.charCodeAt(0);
|
{
|
||||||
|
let result = parseHttp(inputBuffer, inputBuffer.length - data.length);
|
||||||
|
if (result) {
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
if (result == -2) {
|
||||||
|
/* More. */
|
||||||
|
} else {
|
||||||
|
badRequest(client, 'Bad request.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (typeof result === 'object') {
|
||||||
|
request = [
|
||||||
|
result.method,
|
||||||
|
result.path,
|
||||||
|
`HTTP/1.${result.minor_version}`,
|
||||||
|
];
|
||||||
|
|
||||||
var more = true;
|
headers = Object.fromEntries(Object.entries(result.headers).map(x => [x[0].toLowerCase(), x[1]]));
|
||||||
while (more) {
|
parsing_header = false;
|
||||||
if (lineByLine) {
|
inputBuffer = inputBuffer.slice(result.bytes_parsed);
|
||||||
more = false;
|
|
||||||
var end = inputBuffer.indexOf(newLine);
|
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) {
|
||||||
var realEnd = end;
|
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
if (end > 0 && inputBuffer[end - 1] == carriageReturn) {
|
let response = new Response(requestObject, client);
|
||||||
--end;
|
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
|
||||||
|
response.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers["content-length"] != undefined) {
|
||||||
|
bodyToRead = parseInt(headers["content-length"]);
|
||||||
|
if (bodyToRead > 16 * 1024 * 1024) {
|
||||||
|
badRequest(client, 'Request too large: ' + bodyToRead + '.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body = new Uint8Array(bodyToRead);
|
||||||
|
client.info = 'waiting for body';
|
||||||
|
resetTimeout(requestCount);
|
||||||
|
} else if (headers["connection"]
|
||||||
|
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
||||||
|
&& headers["upgrade"]
|
||||||
|
&& headers["upgrade"].toLowerCase() == "websocket") {
|
||||||
|
isWebsocket = true;
|
||||||
|
client.info = 'websocket';
|
||||||
|
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
||||||
|
let response = new Response(requestObject, client);
|
||||||
|
handleWebSocketRequest(requestObject, response, client);
|
||||||
|
/* Prevent the timeout from disconnecting us. */
|
||||||
|
requestCount++;
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (end != -1) {
|
|
||||||
var line = inputBuffer.slice(0, end);
|
|
||||||
inputBuffer = inputBuffer.slice(realEnd + 1);
|
|
||||||
more = handleLine(line, realEnd + 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
more = handleLine(inputBuffer, inputBuffer.length);
|
|
||||||
inputBuffer = new Uint8Array(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!parsing_header && inputBuffer.length)
|
||||||
|
{
|
||||||
|
let offset = body.length - bodyToRead;
|
||||||
|
let length = Math.min(inputBuffer.length, body.length - offset);
|
||||||
|
if (inputBuffer.length > body.length - offset) {
|
||||||
|
body.set(inputBuffer.slice(0, length), offset);
|
||||||
|
inputBuffer = inputBuffer.slice(length);
|
||||||
|
} else {
|
||||||
|
body.set(inputBuffer, offset);
|
||||||
|
inputBuffer = inputBuffer.slice(inputBuffer.length);
|
||||||
|
}
|
||||||
|
bodyToRead -= length;
|
||||||
|
if (bodyToRead <= 0) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.info = 'EOF';
|
||||||
|
client.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var kBacklog = 8;
|
let kBacklog = 8;
|
||||||
var kHost = "0.0.0.0"
|
let kHost = "0.0.0.0"
|
||||||
|
|
||||||
var socket = new Socket();
|
let socket = new Socket();
|
||||||
socket.bind(kHost, tildefriends.http_port).then(function() {
|
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
||||||
var listenResult = socket.listen(kBacklog, function() {
|
print("bound to", port);
|
||||||
socket.accept().then(handleConnection).catch(function(error) {
|
print("checking", tildefriends.args.out_http_port_file);
|
||||||
logError("[" + new Date() + "] accept error " + error);
|
if (tildefriends.args.out_http_port_file) {
|
||||||
|
print("going to write the file");
|
||||||
|
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
||||||
|
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
||||||
|
}).catch(function() {
|
||||||
|
print("failed to write port file");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
let listenResult = socket.listen(kBacklog, async function() {
|
||||||
|
try {
|
||||||
|
let client = await socket.accept();
|
||||||
|
client.noDelay = true;
|
||||||
|
handleConnection(client);
|
||||||
|
} catch (error) {
|
||||||
|
logError("[" + new Date() + "] accept error " + error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
logError("[" + new Date() + "] bind error " + error);
|
logError("[" + new Date() + "] bind error " + error);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tildefriends.https_port) {
|
if (tildefriends.https_port) {
|
||||||
var tls = {};
|
let tls = {};
|
||||||
var secureSocket = new Socket();
|
let 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);
|
let client = await secureSocket.accept();
|
||||||
|
client.noDelay = true;
|
||||||
|
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([
|
let 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));
|
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
||||||
var certificate = new TextDecoder("ASCII").decode(File.readFile(kCertificatePath));
|
let 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,40 @@
|
|||||||
<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">
|
<tf-navigation></tf-navigation>
|
||||||
<span>😎</span>
|
|
||||||
<span id="title">Tilde Friends</span>
|
|
||||||
<a href="/">home</a>
|
|
||||||
<a href="#" onclick="event.preventDefault(); edit()">edit</a>
|
|
||||||
<a href="/trace">trace</a>
|
|
||||||
<span id="status"></span>
|
|
||||||
<span id="login"></span>
|
|
||||||
</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">
|
||||||
|
</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">
|
||||||
<input type="button" id="save" name="save" value="Save" onclick="save()">
|
<input type="button" id="save" name="save" value="Save">
|
||||||
<input type="text" id="name" name="name"></input>
|
<input type="button" id="icon" name="icon" value="📦">
|
||||||
<input type="checkbox" id="run" name="run" checked><label for="run">Restart after save</label>
|
<input type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
||||||
<input type="button" id="revert" name="revert" value="Revert to Saved" onclick="revert()">
|
<input type="button" id="delete" name="delete" value="Delete">
|
||||||
<a id="latest" href="">Latest</a>
|
<input type="button" id="trace_button" value="Trace">
|
||||||
|
<input type="button" id="stats_button" 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">«</span>
|
||||||
<div><button onclick="newFile()">New File</button></div>
|
<span id="files_show">»</span>
|
||||||
<div><button onclick="removeFile()">Remove File</button></div>
|
</div>
|
||||||
|
<div id="files_content">
|
||||||
|
<tf-files id="files_list"></tf-files>
|
||||||
|
<ul id="files"></ul>
|
||||||
|
<br>
|
||||||
|
<div><button id="new_file_button">New File</button></div>
|
||||||
|
<div><button id="remove_file_button">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 +47,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="/static/client.js"></script>
|
<script src="/split/split.min.js"></script>
|
||||||
|
<script src="/smoothie/smoothie.js"></script>
|
||||||
|
<script src="/static/client.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
160
core/sha1.js
160
core/sha1.js
@ -1,160 +0,0 @@
|
|||||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
||||||
/* SHA-1 implementation in JavaScript (c) Chris Veness 2002-2014 / MIT Licence */
|
|
||||||
/* */
|
|
||||||
/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */
|
|
||||||
/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */
|
|
||||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
||||||
|
|
||||||
/* jshint node:true *//* global define, escape, unescape */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SHA-1 hash function reference implementation.
|
|
||||||
*
|
|
||||||
* @namespace
|
|
||||||
*/
|
|
||||||
var Sha1 = {};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates SHA-1 hash of string.
|
|
||||||
*
|
|
||||||
* @param {string} msg - (Unicode) string to be hashed.
|
|
||||||
* @returns {string} Hash of msg as hex character string.
|
|
||||||
*/
|
|
||||||
Sha1.hash = function(msg) {
|
|
||||||
// convert string to UTF-8, as SHA only deals with byte-streams
|
|
||||||
msg = msg.utf8Encode();
|
|
||||||
|
|
||||||
// constants [§4.2.1]
|
|
||||||
var K = [ 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 ];
|
|
||||||
|
|
||||||
// PREPROCESSING
|
|
||||||
|
|
||||||
msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1]
|
|
||||||
|
|
||||||
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
|
|
||||||
var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
|
|
||||||
var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints
|
|
||||||
var M = new Array(N);
|
|
||||||
|
|
||||||
for (var i=0; i<N; i++) {
|
|
||||||
M[i] = new Array(16);
|
|
||||||
for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding
|
|
||||||
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
|
|
||||||
(msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
|
|
||||||
} // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
|
|
||||||
}
|
|
||||||
// add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
|
|
||||||
// note: most significant word would be (len-1)*8 >>> 32, but since JS converts
|
|
||||||
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
|
|
||||||
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]);
|
|
||||||
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
|
|
||||||
|
|
||||||
// set initial hash value [§5.3.1]
|
|
||||||
var H0 = 0x67452301;
|
|
||||||
var H1 = 0xefcdab89;
|
|
||||||
var H2 = 0x98badcfe;
|
|
||||||
var H3 = 0x10325476;
|
|
||||||
var H4 = 0xc3d2e1f0;
|
|
||||||
|
|
||||||
// HASH COMPUTATION [§6.1.2]
|
|
||||||
|
|
||||||
var W = new Array(80); var a, b, c, d, e;
|
|
||||||
for (var i=0; i<N; i++) {
|
|
||||||
|
|
||||||
// 1 - prepare message schedule 'W'
|
|
||||||
for (var t=0; t<16; t++) W[t] = M[i][t];
|
|
||||||
for (var t=16; t<80; t++) W[t] = Sha1.ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
|
|
||||||
|
|
||||||
// 2 - initialise five working variables a, b, c, d, e with previous hash value
|
|
||||||
a = H0; b = H1; c = H2; d = H3; e = H4;
|
|
||||||
|
|
||||||
// 3 - main loop
|
|
||||||
for (var t=0; t<80; t++) {
|
|
||||||
var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
|
|
||||||
var T = (Sha1.ROTL(a,5) + Sha1.f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
|
|
||||||
e = d;
|
|
||||||
d = c;
|
|
||||||
c = Sha1.ROTL(b, 30);
|
|
||||||
b = a;
|
|
||||||
a = T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4 - compute the new intermediate hash value (note 'addition modulo 2^32')
|
|
||||||
H0 = (H0+a) & 0xffffffff;
|
|
||||||
H1 = (H1+b) & 0xffffffff;
|
|
||||||
H2 = (H2+c) & 0xffffffff;
|
|
||||||
H3 = (H3+d) & 0xffffffff;
|
|
||||||
H4 = (H4+e) & 0xffffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Sha1.toHexStr(H0) + Sha1.toHexStr(H1) + Sha1.toHexStr(H2) +
|
|
||||||
Sha1.toHexStr(H3) + Sha1.toHexStr(H4);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function 'f' [§4.1.1].
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Sha1.f = function(s, x, y, z) {
|
|
||||||
switch (s) {
|
|
||||||
case 0: return (x & y) ^ (~x & z); // Ch()
|
|
||||||
case 1: return x ^ y ^ z; // Parity()
|
|
||||||
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
|
|
||||||
case 3: return x ^ y ^ z; // Parity()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rotates left (circular left shift) value x by n positions [§3.2.5].
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Sha1.ROTL = function(x, n) {
|
|
||||||
return (x<<n) | (x>>>(32-n));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hexadecimal representation of a number.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Sha1.toHexStr = function(n) {
|
|
||||||
// note can't use toString(16) as it is implementation-dependant,
|
|
||||||
// and in IE returns signed numbers when used on full words
|
|
||||||
var s="", v;
|
|
||||||
for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); }
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
||||||
|
|
||||||
|
|
||||||
/** Extend String object with method to encode multi-byte string to utf8
|
|
||||||
* - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */
|
|
||||||
if (typeof String.prototype.utf8Encode == 'undefined') {
|
|
||||||
String.prototype.utf8Encode = function() {
|
|
||||||
return unescape( encodeURIComponent( this ) );
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Extend String object with method to decode utf8 string to multi-byte */
|
|
||||||
if (typeof String.prototype.utf8Decode == 'undefined') {
|
|
||||||
String.prototype.utf8Decode = function() {
|
|
||||||
try {
|
|
||||||
return decodeURIComponent( escape( this ) );
|
|
||||||
} catch (e) {
|
|
||||||
return this; // invalid UTF-8? return as-is
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
|
||||||
if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export
|
|
||||||
if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD
|
|
||||||
|
|
||||||
exports.hash = Sha1.hash;
|
|
118
core/style.css
118
core/style.css
@ -15,9 +15,20 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
a:link {
|
||||||
height: auto;
|
color: #268bd2;
|
||||||
margin: 4px;
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #2aa198;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
#logo {
|
||||||
@ -146,17 +157,94 @@ body {
|
|||||||
.cyan { color: #2aa198; }
|
.cyan { color: #2aa198; }
|
||||||
.green { color: #859900; }
|
.green { color: #859900; }
|
||||||
|
|
||||||
#files {
|
#files_header {
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files > li {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#files > li.current {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: #2aa198;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
90
core/tfrpc.js
Normal file
90
core/tfrpc.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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] !== undefined) {
|
||||||
|
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') {
|
||||||
|
let method = g_api[message.method];
|
||||||
|
let id = message.id;
|
||||||
|
if (message.method) {
|
||||||
|
if (method) {
|
||||||
|
try {
|
||||||
|
Promise.resolve(method(...message.params)).then(function(result) {
|
||||||
|
send({message: 'tfrpc', id: id, result: result});
|
||||||
|
}).catch(function(error) {
|
||||||
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(message.method + ' not found.');
|
||||||
|
}
|
||||||
|
} else if (message.error !== undefined) {
|
||||||
|
if (g_calls[id]) {
|
||||||
|
g_calls[id].reject(message.error);
|
||||||
|
delete g_calls[id];
|
||||||
|
} else {
|
||||||
|
throw new Error(id + ' not found to reply.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (g_calls[id]) {
|
||||||
|
g_calls[id].resolve(message.result);
|
||||||
|
delete g_calls[id];
|
||||||
|
} else {
|
||||||
|
throw new Error(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;
|
||||||
|
}
|
106
deps/base64c/.gitignore
vendored
106
deps/base64c/.gitignore
vendored
@ -1,106 +0,0 @@
|
|||||||
# Prerequisites
|
|
||||||
*.d
|
|
||||||
|
|
||||||
# Object files
|
|
||||||
*.o
|
|
||||||
*.ko
|
|
||||||
*.obj
|
|
||||||
*.elf
|
|
||||||
|
|
||||||
# Linker output
|
|
||||||
*.ilk
|
|
||||||
*.map
|
|
||||||
*.exp
|
|
||||||
|
|
||||||
# Precompiled Headers
|
|
||||||
*.gch
|
|
||||||
*.pch
|
|
||||||
|
|
||||||
# Libraries
|
|
||||||
*.lib
|
|
||||||
*.a
|
|
||||||
*.la
|
|
||||||
*.lo
|
|
||||||
|
|
||||||
# Shared objects (inc. Windows DLLs)
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.so.*
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Executables
|
|
||||||
*.exe
|
|
||||||
*.out
|
|
||||||
*.app
|
|
||||||
*.i*86
|
|
||||||
*.x86_64
|
|
||||||
*.hex
|
|
||||||
|
|
||||||
# Debug files
|
|
||||||
*.dSYM/
|
|
||||||
*.su
|
|
||||||
*.idb
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
# Kernel Module Compile Results
|
|
||||||
*.mod*
|
|
||||||
*.cmd
|
|
||||||
.tmp_versions/
|
|
||||||
modules.order
|
|
||||||
Module.symvers
|
|
||||||
Mkfile.old
|
|
||||||
dkms.conf
|
|
||||||
|
|
||||||
# http://www.gnu.org/software/automake
|
|
||||||
Makefile
|
|
||||||
Makefile.in
|
|
||||||
/ar-lib
|
|
||||||
/mdate-sh
|
|
||||||
/py-compile
|
|
||||||
/test-driver
|
|
||||||
/ylwrap
|
|
||||||
|
|
||||||
# http://www.gnu.org/software/autoheader
|
|
||||||
config.h
|
|
||||||
# http://www.gnu.org/software/autoconf
|
|
||||||
|
|
||||||
autom4te.cache
|
|
||||||
/autoscan.log
|
|
||||||
/autoscan-*.log
|
|
||||||
/aclocal.m4
|
|
||||||
/compile
|
|
||||||
/config.guess
|
|
||||||
/config.h.in
|
|
||||||
/config.log
|
|
||||||
/config.status
|
|
||||||
/config.sub
|
|
||||||
/configure
|
|
||||||
/configure.scan
|
|
||||||
/depcomp
|
|
||||||
/install-sh
|
|
||||||
/missing
|
|
||||||
/stamp-h1
|
|
||||||
|
|
||||||
# https://www.gnu.org/software/libtool/
|
|
||||||
|
|
||||||
/ltmain.sh
|
|
||||||
|
|
||||||
# http://www.gnu.org/software/texinfo
|
|
||||||
|
|
||||||
/texinfo.tex
|
|
||||||
|
|
||||||
# http://www.gnu.org/software/m4/
|
|
||||||
|
|
||||||
m4/libtool.m4
|
|
||||||
m4/ltoptions.m4
|
|
||||||
m4/ltsugar.m4
|
|
||||||
m4/ltversion.m4
|
|
||||||
m4/lt~obsolete.m4
|
|
||||||
|
|
||||||
# vim
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# project specific
|
|
||||||
test/gen
|
|
||||||
test/test[0-9]*
|
|
||||||
test/.deps
|
|
29
deps/base64c/LICENSE
vendored
29
deps/base64c/LICENSE
vendored
@ -1,29 +0,0 @@
|
|||||||
BSD 3-Clause License
|
|
||||||
|
|
||||||
Copyright (c) 2018, Sean Hanna
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* 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.
|
|
||||||
|
|
||||||
* Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.
|
|
2
deps/base64c/Makefile.am
vendored
2
deps/base64c/Makefile.am
vendored
@ -1,2 +0,0 @@
|
|||||||
AUTOMAKE_OPTIONS = foreign
|
|
||||||
SUBDIRS = src test
|
|
60
deps/base64c/README.md
vendored
60
deps/base64c/README.md
vendored
@ -1,60 +0,0 @@
|
|||||||
# base64c
|
|
||||||
This is primarily just a fork of a base64 decoder from the FreeBSD codebase. It has received a few modifications:
|
|
||||||
* removed all allocations, you are expected to pass in a buffer that has sufficient space and you will get an error (-1) if you run out of space
|
|
||||||
* replaced a dynamically generated lookup table with a hardcoded lookup table
|
|
||||||
* wrote my own unit tests, i'm sure there are tests for freebsd somewhere but i didn't find them
|
|
||||||
|
|
||||||
# Embedding
|
|
||||||
This code is primarily intended to be dropped into an existing code base ( or perhaps using submodules). To do that:
|
|
||||||
|
|
||||||
* grab include/base64c.h
|
|
||||||
* grab src/base64c.h
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
Call base64c_encoding_length() to calculate how big a buffer you need to encode a string. It's somewhere around 4 times the size of the input string. This length includes a null terminator.
|
|
||||||
|
|
||||||
```c
|
|
||||||
char input_string[256];
|
|
||||||
|
|
||||||
size_t new_len = base64c_encoding_length( strlen(input_string));
|
|
||||||
|
|
||||||
unsigned char *buffer = (unsigned char*)malloc(new_len);
|
|
||||||
```
|
|
||||||
|
|
||||||
Call base64c_encode() to actually encode your input string as base64. It will write to the buffer and return how many characters were written. If there was an error it will return -1.
|
|
||||||
|
|
||||||
```c
|
|
||||||
size_t output_length = base64c_encode(input_string, strlen(input_string), buffer, new_len);
|
|
||||||
|
|
||||||
if (output_length == -1) {
|
|
||||||
int x = 1/0; // ERROR!
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Call base64c_decoding_length() to calculate how big a buffer you need to decode. It comes out to about half the size. This number isn't always exact, but it is close to within a byte or two.
|
|
||||||
|
|
||||||
```c
|
|
||||||
size_t decode_len = base64c_decoding_length( strlen(buffer) );
|
|
||||||
|
|
||||||
unsigned char *decoded = (unsigned char*)malloc( decode_len );
|
|
||||||
```
|
|
||||||
|
|
||||||
Call base64c_decode() to decode an encoded base64 string. It will write to the buffer and return how many characters were written. IF there was an error it will return -1. If the string contains invalid number of characters, or has any characters that are not part of the base64 character set an error will be returned.
|
|
||||||
|
|
||||||
# Building
|
|
||||||
|
|
||||||
You need to bootstrap all the autoconf tools by running ./autogen.sh
|
|
||||||
|
|
||||||
You need to have autoconf installed to do this.
|
|
||||||
|
|
||||||
Once bootstrapped run ./configure
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
|
|
||||||
There are tests in the test/ subfolder. They will be built automatically. There is no special test runner. You can run each of the test cases manually to check whether the code is working properly.
|
|
||||||
|
|
||||||
# References
|
|
||||||
|
|
||||||
(http://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c)
|
|
||||||
(https://github.com/freebsd/freebsd/blob/master/contrib/wpa/src/utils/base64.c)
|
|
3
deps/base64c/autogen.sh
vendored
3
deps/base64c/autogen.sh
vendored
@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
aclocal && automake --gnu --add-missing && autoconf
|
|
22
deps/base64c/configure.ac
vendored
22
deps/base64c/configure.ac
vendored
@ -1,22 +0,0 @@
|
|||||||
# -*- Autoconf -*-
|
|
||||||
# Process this file with autoconf to produce a configure script.
|
|
||||||
BASE64C_VERSION=0.5
|
|
||||||
AC_PREREQ([2.69])
|
|
||||||
AC_INIT(base64c, 0.5, hannasm@gmail.com)
|
|
||||||
AM_INIT_AUTOMAKE(base64c, 0.5)
|
|
||||||
AC_CONFIG_SRCDIR([include/base64c.h])
|
|
||||||
AC_CONFIG_HEADERS([config.h])
|
|
||||||
|
|
||||||
# Checks for programs.
|
|
||||||
AC_PROG_CC
|
|
||||||
|
|
||||||
# Checks for libraries.
|
|
||||||
|
|
||||||
# Checks for header files.
|
|
||||||
|
|
||||||
# Checks for typedefs, structures, and compiler characteristics.
|
|
||||||
AC_TYPE_SIZE_T
|
|
||||||
|
|
||||||
# Checks for library functions.
|
|
||||||
|
|
||||||
AC_OUTPUT(Makefile src/Makefile test/Makefile)
|
|
42
deps/base64c/include/base64c.h
vendored
42
deps/base64c/include/base64c.h
vendored
@ -1,42 +0,0 @@
|
|||||||
#ifndef base64cC_H
|
|
||||||
#define base64cC_H
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64c_encoding_length - calculate length to allocate for encode
|
|
||||||
* @len: Length of input string
|
|
||||||
* Returns: number of bytes required to base64c encode, this includes room for '\0' terminator
|
|
||||||
*/
|
|
||||||
size_t base64c_encoding_length(size_t len);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64c_decoding_length - calculate length to allocate for decode
|
|
||||||
* @len: Length of (base64 encoded) input string
|
|
||||||
* Returns: maximum number of bytes required to decode
|
|
||||||
*/
|
|
||||||
size_t base64c_decoding_length(size_t inlen);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64c_encode - base64c encode
|
|
||||||
* @src: Data to be encoded
|
|
||||||
* @len: Length of the data to be encoded
|
|
||||||
* @out: Mutable output buffer destination, all encoded bytes will be written to the destination
|
|
||||||
* @out_len: length of output buffer
|
|
||||||
* Returns: number of bytes written, or -1 if there was an error
|
|
||||||
*/
|
|
||||||
size_t base64c_encode(const unsigned char *src, size_t len, unsigned char* out, const size_t out_len);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* base64c_decode - base64c decode
|
|
||||||
* @src: Data to be decoded
|
|
||||||
* @len: Length of the data to be decoded
|
|
||||||
* @out_len: Pointer to output length variable
|
|
||||||
* Returns: Allocated buffer of out_len bytes of decoded data,
|
|
||||||
* or %NULL on failure
|
|
||||||
*
|
|
||||||
* Caller is responsible for freeing the returned buffer.
|
|
||||||
*/
|
|
||||||
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len);
|
|
||||||
#endif
|
|
3
deps/base64c/src/Makefile.am
vendored
3
deps/base64c/src/Makefile.am
vendored
@ -1,3 +0,0 @@
|
|||||||
CFLAGS = --pednatic -Wall -stdc99 -O2
|
|
||||||
LDFLAGS =
|
|
||||||
|
|
139
deps/base64c/src/base64c.c
vendored
139
deps/base64c/src/base64c.c
vendored
@ -1,139 +0,0 @@
|
|||||||
#include <string.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Base64 encoding/decoding (RFC1341)
|
|
||||||
* Copyright (c) 2005-2011, Jouni Malinen <j@w1.fi>
|
|
||||||
*
|
|
||||||
* This software may be distributed under the terms of the BSD license.
|
|
||||||
* See README for more details.
|
|
||||||
*/
|
|
||||||
static const unsigned char base64c_table[65] =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
static const unsigned char base64c_dtable[256] = {
|
|
||||||
/*000*/0x80,/*001*/0x80,/*002*/0x80,/*003*/0x80,/*004*/0x80,/*005*/0x80,/*006*/0x80,/*007*/0x80,/*008*/0x80,/*009*/0x80,/*010*/0x80,/*011*/0x80,/*012*/0x80,/*013*/0x80,/*014*/0x80,/*015*/0x80,/*016*/0x80,/*017*/0x80,/*018*/0x80,/*019*/0x80,
|
|
||||||
/*020*/0x80,/*021*/0x80,/*022*/0x80,/*023*/0x80,/*024*/0x80,/*025*/0x80,/*026*/0x80,/*027*/0x80,/*028*/0x80,/*029*/0x80,/*030*/0x80,/*031*/0x80,/*032*/0x80,/*033*/0x80,/*034*/0x80,/*035*/0x80,/*036*/0x80,/*037*/0x80,/*038*/0x80,/*039*/0x80,
|
|
||||||
/*040*/0x80,/*041*/0x80,/*042*/0x80,/*043*/0x3e,/*044*/0x80,/*045*/0x80,/*046*/0x80,/*047*/0x3f,/*048*/0x34,/*049*/0x35,/*050*/0x36,/*051*/0x37,/*052*/0x38,/*053*/0x39,/*054*/0x3a,/*055*/0x3b,/*056*/0x3c,/*057*/0x3d,/*058*/0x80,/*059*/0x80,
|
|
||||||
/*060*/0x80,/*061*/0x00,/*062*/0x80,/*063*/0x80,/*064*/0x80,/*065*/0x00,/*066*/0x01,/*067*/0x02,/*068*/0x03,/*069*/0x04,/*070*/0x05,/*071*/0x06,/*072*/0x07,/*073*/0x08,/*074*/0x09,/*075*/0x0a,/*076*/0x0b,/*077*/0x0c,/*078*/0x0d,/*079*/0x0e,
|
|
||||||
/*080*/0x0f,/*081*/0x10,/*082*/0x11,/*083*/0x12,/*084*/0x13,/*085*/0x14,/*086*/0x15,/*087*/0x16,/*088*/0x17,/*089*/0x18,/*090*/0x19,/*091*/0x80,/*092*/0x80,/*093*/0x80,/*094*/0x80,/*095*/0x80,/*096*/0x80,/*097*/0x1a,/*098*/0x1b,/*099*/0x1c,
|
|
||||||
/*100*/0x1d,/*101*/0x1e,/*102*/0x1f,/*103*/0x20,/*104*/0x21,/*105*/0x22,/*106*/0x23,/*107*/0x24,/*108*/0x25,/*109*/0x26,/*110*/0x27,/*111*/0x28,/*112*/0x29,/*113*/0x2a,/*114*/0x2b,/*115*/0x2c,/*116*/0x2d,/*117*/0x2e,/*118*/0x2f,/*119*/0x30,
|
|
||||||
/*120*/0x31,/*121*/0x32,/*122*/0x33,/*123*/0x80,/*124*/0x80,/*125*/0x80,/*126*/0x80,/*127*/0x80,/*128*/0x80,/*129*/0x80,/*130*/0x80,/*131*/0x80,/*132*/0x80,/*133*/0x80,/*134*/0x80,/*135*/0x80,/*136*/0x80,/*137*/0x80,/*138*/0x80,/*139*/0x80,
|
|
||||||
/*140*/0x80,/*141*/0x80,/*142*/0x80,/*143*/0x80,/*144*/0x80,/*145*/0x80,/*146*/0x80,/*147*/0x80,/*148*/0x80,/*149*/0x80,/*150*/0x80,/*151*/0x80,/*152*/0x80,/*153*/0x80,/*154*/0x80,/*155*/0x80,/*156*/0x80,/*157*/0x80,/*158*/0x80,/*159*/0x80,
|
|
||||||
/*160*/0x80,/*161*/0x80,/*162*/0x80,/*163*/0x80,/*164*/0x80,/*165*/0x80,/*166*/0x80,/*167*/0x80,/*168*/0x80,/*169*/0x80,/*170*/0x80,/*171*/0x80,/*172*/0x80,/*173*/0x80,/*174*/0x80,/*175*/0x80,/*176*/0x80,/*177*/0x80,/*178*/0x80,/*179*/0x80,
|
|
||||||
/*180*/0x80,/*181*/0x80,/*182*/0x80,/*183*/0x80,/*184*/0x80,/*185*/0x80,/*186*/0x80,/*187*/0x80,/*188*/0x80,/*189*/0x80,/*190*/0x80,/*191*/0x80,/*192*/0x80,/*193*/0x80,/*194*/0x80,/*195*/0x80,/*196*/0x80,/*197*/0x80,/*198*/0x80,/*199*/0x80,
|
|
||||||
/*200*/0x80,/*201*/0x80,/*202*/0x80,/*203*/0x80,/*204*/0x80,/*205*/0x80,/*206*/0x80,/*207*/0x80,/*208*/0x80,/*209*/0x80,/*210*/0x80,/*211*/0x80,/*212*/0x80,/*213*/0x80,/*214*/0x80,/*215*/0x80,/*216*/0x80,/*217*/0x80,/*218*/0x80,/*219*/0x80,
|
|
||||||
/*220*/0x80,/*221*/0x80,/*222*/0x80,/*223*/0x80,/*224*/0x80,/*225*/0x80,/*226*/0x80,/*227*/0x80,/*228*/0x80,/*229*/0x80,/*230*/0x80,/*231*/0x80,/*232*/0x80,/*233*/0x80,/*234*/0x80,/*235*/0x80,/*236*/0x80,/*237*/0x80,/*238*/0x80,/*239*/0x80,
|
|
||||||
/*240*/0x80,/*241*/0x80,/*242*/0x80,/*243*/0x80,/*244*/0x80,/*245*/0x80,/*246*/0x80,/*247*/0x80,/*248*/0x80,/*249*/0x80,/*250*/0x80,/*251*/0x80,/*252*/0x80,/*253*/0x80,/*254*/0x80,/*255*/0x00,
|
|
||||||
};
|
|
||||||
|
|
||||||
size_t base64c_encoding_length(size_t len) {
|
|
||||||
size_t olen = len * 4 / 3 + 4; /* 3-byte blocks to 4-byte */
|
|
||||||
olen++; /* nul termination */
|
|
||||||
if (olen < len)
|
|
||||||
return 0; /* integer overflow */
|
|
||||||
return olen;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t base64c_encode(const unsigned char *src, size_t len,
|
|
||||||
unsigned char* out, const size_t out_len)
|
|
||||||
{
|
|
||||||
unsigned char *pos;
|
|
||||||
const unsigned char *end, *in;
|
|
||||||
const unsigned char *out_end = out + out_len;
|
|
||||||
|
|
||||||
end = src + len;
|
|
||||||
in = src;
|
|
||||||
pos = out;
|
|
||||||
|
|
||||||
if (out_len < base64c_encoding_length(len)) { return -1; }
|
|
||||||
|
|
||||||
while (end - in >= 3 ) {
|
|
||||||
*pos++ = base64c_table[in[0] >> 2];
|
|
||||||
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
|
|
||||||
*pos++ = base64c_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
|
|
||||||
*pos++ = base64c_table[in[2] & 0x3f];
|
|
||||||
in += 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end - in) {
|
|
||||||
*pos++ = base64c_table[in[0] >> 2];
|
|
||||||
|
|
||||||
if (end - in == 1) {
|
|
||||||
*pos++ = base64c_table[(in[0] & 0x03) << 4];
|
|
||||||
*pos++ = '=';
|
|
||||||
} else {
|
|
||||||
*pos++ = base64c_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
|
|
||||||
*pos++ = base64c_table[(in[1] & 0x0f) << 2];
|
|
||||||
}
|
|
||||||
*pos++ = '=';
|
|
||||||
}
|
|
||||||
|
|
||||||
*pos = '\0';
|
|
||||||
|
|
||||||
return out_len - (out_end-pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t base64c_decoding_length(size_t inlen) {
|
|
||||||
return inlen / 4 * 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t base64c_decode(const unsigned char *src, size_t len, unsigned char *out, const size_t out_len)
|
|
||||||
{
|
|
||||||
if (out == NULL) { return 0; }
|
|
||||||
if (out_len <= 0) { return 0; }
|
|
||||||
|
|
||||||
unsigned char *pos, block[4], tmp;
|
|
||||||
size_t i, count;
|
|
||||||
int pad = 0;
|
|
||||||
|
|
||||||
if (len == 0 ){
|
|
||||||
*out = '\0';
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (len % 4) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = out;
|
|
||||||
count = 0;
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
if (src[i] == '=') { pad++; }
|
|
||||||
tmp = base64c_dtable[src[i]];
|
|
||||||
|
|
||||||
if (tmp == 0x80) { return -1; }
|
|
||||||
|
|
||||||
block[count] = tmp;
|
|
||||||
count++;
|
|
||||||
if (count == 4) {
|
|
||||||
switch (pad) {
|
|
||||||
case 0:
|
|
||||||
if ((pos - out) + 3 > out_len) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
|
||||||
*pos++ = (block[1] << 4) | (block[2] >> 2);
|
|
||||||
*pos++ = (block[2] << 6) | block[3];
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
if ((pos - out) + 2 > out_len || i + 1 > len) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
|
||||||
*pos++ = (block[1] << 4) | (block[2] >> 2);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
if ((pos - out) + 1 > out_len || i + 1 > len) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*pos++ = (block[0] << 2) | (block[1] >> 4);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
count = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pos - out;
|
|
||||||
}
|
|
16
deps/base64c/test/Makefile.am
vendored
16
deps/base64c/test/Makefile.am
vendored
@ -1,16 +0,0 @@
|
|||||||
CFLAGS = --pedantic -Wall -std=c99 -g -ggdb
|
|
||||||
LDFLAGS =
|
|
||||||
|
|
||||||
bin_PROGRAMS = test001 test002 test003 test004 \
|
|
||||||
test005 test006 test007 test008 \
|
|
||||||
gen
|
|
||||||
|
|
||||||
test001_SOURCES = test001.c ../src/base64c.c
|
|
||||||
test002_SOURCES = test002.c ../src/base64c.c
|
|
||||||
test003_SOURCES = test003.c ../src/base64c.c
|
|
||||||
test004_SOURCES = test004.c ../src/base64c.c
|
|
||||||
test005_SOURCES = test005.c ../src/base64c.c
|
|
||||||
test006_SOURCES = test006.c ../src/base64c.c
|
|
||||||
test007_SOURCES = test007.c ../src/base64c.c
|
|
||||||
test008_SOURCES = test008.c ../src/base64c.c
|
|
||||||
gen_SOURCES = gen.c
|
|
22
deps/base64c/test/gen.c
vendored
22
deps/base64c/test/gen.c
vendored
@ -1,22 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
static const unsigned char base64_table[65] =
|
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
unsigned char out[256];
|
|
||||||
|
|
||||||
memset(out, 0x80, 255);
|
|
||||||
for (int i = 0; i < 64; i++) {
|
|
||||||
out[base64_table[i]] = i;
|
|
||||||
}
|
|
||||||
out['='] = 0;
|
|
||||||
|
|
||||||
printf("static const unsigned char base64c_dtable[256] = {");
|
|
||||||
for (int i = 0; i < 256; i++) {
|
|
||||||
if (i% 20==0) { printf("\n"); }
|
|
||||||
printf("/*%03d*/0x%02x,", i, out[i]);
|
|
||||||
}
|
|
||||||
printf("\n};");
|
|
||||||
}
|
|
36
deps/base64c/test/test001.c
vendored
36
deps/base64c/test/test001.c
vendored
@ -1,36 +0,0 @@
|
|||||||
#include "../include/base64c.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
unsigned char in[12] = "Hello World";
|
|
||||||
size_t in_len = 11;
|
|
||||||
unsigned char enc[32];
|
|
||||||
size_t enc_len = 32;
|
|
||||||
unsigned char out[12];
|
|
||||||
size_t out_len = 12;
|
|
||||||
|
|
||||||
printf("Encoding %lu - %s\n", in_len, in);
|
|
||||||
|
|
||||||
size_t enc_result = base64c_encode(in, in_len, enc, enc_len);
|
|
||||||
|
|
||||||
printf("Encoded %lu - %s\n", enc_result, enc);
|
|
||||||
|
|
||||||
size_t dec_result = base64c_decode(enc, enc_result, out, out_len);
|
|
||||||
|
|
||||||
if ((long)dec_result < 0) {
|
|
||||||
printf("Decode failed with code %ld\n", (long)dec_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Decoded %lu - %s\n", dec_result, out);
|
|
||||||
|
|
||||||
if (dec_result != in_len) {
|
|
||||||
printf("in length %ld not equal to out length %ld", in_len, dec_result);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (strncmp((char*)in, (char*)out, in_len)) {
|
|
||||||
printf("roundtrip encoding failed\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
37
deps/base64c/test/test002.c
vendored
37
deps/base64c/test/test002.c
vendored
@ -1,37 +0,0 @@
|
|||||||
#include "../include/base64c.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
unsigned char in[11] = "Hello Worl";
|
|
||||||
size_t in_len = 10;
|
|
||||||
unsigned char enc[32];
|
|
||||||
size_t enc_len = 32;
|
|
||||||
unsigned char out[12];
|
|
||||||
size_t out_len = 12;
|
|
||||||
|
|
||||||
printf("Encoding %lu - %s\n", in_len, in);
|
|
||||||
|
|
||||||
size_t enc_result = base64c_encode(in, in_len, enc, enc_len);
|
|
||||||
|
|
||||||
printf("Encoded %lu - %s\n", enc_result, enc);
|
|
||||||
|
|
||||||
size_t dec_result = base64c_decode(enc, enc_result, out, out_len);
|
|
||||||
|
|
||||||
if ((long)dec_result < 0) {
|
|
||||||
printf("Decode failed with code %ld\n", (long)dec_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Decoded %lu - %s\n", dec_result, out);
|
|
||||||
|
|
||||||
if (dec_result != in_len) {
|
|
||||||
printf("in length %ld not equal to out length %ld", in_len, dec_result);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strncmp((char*)in, (char*)out, in_len)) {
|
|
||||||
printf("roundtrip encoding failed\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
37
deps/base64c/test/test003.c
vendored
37
deps/base64c/test/test003.c
vendored
@ -1,37 +0,0 @@
|
|||||||
#include "../include/base64c.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
unsigned char in[10] = "Hello Wor";
|
|
||||||
size_t in_len = 9;
|
|
||||||
unsigned char enc[32];
|
|
||||||
size_t enc_len = 32;
|
|
||||||
unsigned char out[12];
|
|
||||||
size_t out_len = 12;
|
|
||||||
|
|
||||||
printf("Encoding %lu - %s\n", in_len, in);
|
|
||||||
|
|
||||||
size_t enc_result = base64c_encode(in, in_len, enc, enc_len);
|
|
||||||
|
|
||||||
printf("Encoded %lu - %s\n", enc_result, enc);
|
|
||||||
|
|
||||||
size_t dec_result = base64c_decode(enc, enc_result, out, out_len);
|
|
||||||
|
|
||||||
if ((long)dec_result < 0) {
|
|
||||||
printf("Decode failed with code %ld\n", (long)dec_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Decoded %lu - %s\n", dec_result, out);
|
|
||||||
|
|
||||||
if (dec_result != in_len) {
|
|
||||||
printf("in length %ld not equal to out length %ld", in_len, dec_result);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strncmp((char*)in, (char*)out, in_len)) {
|
|
||||||
printf("roundtrip encoding failed\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
37
deps/base64c/test/test004.c
vendored
37
deps/base64c/test/test004.c
vendored
@ -1,37 +0,0 @@
|
|||||||
#include "../include/base64c.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
unsigned char in[10] = "Hello Wo";
|
|
||||||
size_t in_len = 8;
|
|
||||||
unsigned char enc[32];
|
|
||||||
size_t enc_len = 32;
|
|
||||||
unsigned char out[12];
|
|
||||||
size_t out_len = 12;
|
|
||||||
|
|
||||||
printf("Encoding %lu - %s\n", in_len, in);
|
|
||||||
|
|
||||||
size_t enc_result = base64c_encode(in, in_len, enc, enc_len);
|
|
||||||
|
|
||||||
printf("Encoded %lu - %s\n", enc_result, enc);
|
|
||||||
|
|
||||||
size_t dec_result = base64c_decode(enc, enc_result, out, out_len);
|
|
||||||
|
|
||||||
if ((long)dec_result < 0) {
|
|
||||||
printf("Decode failed with code %ld\n", (long)dec_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Decoded %lu - %s\n", dec_result, out);
|
|
||||||
|
|
||||||
if (dec_result != in_len) {
|
|
||||||
printf("in length %ld not equal to out length %ld", in_len, dec_result);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strncmp((char*)in, (char*)out, in_len)) {
|
|
||||||
printf("roundtrip encoding failed\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
36
deps/base64c/test/test005.c
vendored
36
deps/base64c/test/test005.c
vendored
@ -1,36 +0,0 @@
|
|||||||
#include "../include/base64c.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
unsigned char in[13] = "Hello Worlds";
|
|
||||||
size_t in_len = 12;
|
|
||||||
unsigned char enc[32];
|
|
||||||
size_t enc_len = 32;
|
|
||||||
unsigned char out[13];
|
|
||||||
size_t out_len = 13;
|
|
||||||
|
|
||||||
printf("Encoding %lu - %s\n", in_len, in);
|
|
||||||
|
|
||||||
size_t enc_result = base64c_encode(in, in_len, enc, enc_len);
|
|
||||||
|
|
||||||
printf("Encoded %lu - %s\n", enc_result, enc);
|
|
||||||
|
|
||||||
size_t dec_result = base64c_decode(enc, enc_result, out, out_len);
|
|
||||||
|
|
||||||
if ((long)dec_result < 0) {
|
|
||||||
printf("Decode failed with code %ld\n", (long)dec_result);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("Decoded %lu - %s\n", dec_result, out);
|
|
||||||
|
|
||||||
if (dec_result != in_len) {
|
|
||||||
printf("in length %ld not equal to out length %ld", in_len, dec_result);
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
if (strncmp((char*)in, (char*)out, in_len)) {
|
|
||||||
printf("roundtrip encoding failed\n");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user